cctally 1.22.3 → 1.23.0
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.
- package/CHANGELOG.md +19 -0
- package/bin/_cctally_alerts.py +27 -0
- package/bin/_cctally_config.py +68 -9
- package/bin/_cctally_core.py +60 -2
- package/bin/_cctally_dashboard.py +225 -60
- package/bin/_cctally_forecast.py +25 -3
- package/bin/_cctally_milestones.py +68 -0
- package/bin/_cctally_parser.py +10 -2
- package/bin/_cctally_record.py +472 -138
- package/bin/_cctally_tui.py +1 -0
- package/bin/_cctally_weekrefs.py +36 -2
- package/bin/_lib_alert_axes.py +44 -0
- package/bin/_lib_alerts_payload.py +67 -0
- package/bin/_lib_budget.py +8 -0
- package/bin/_lib_diff_kernel.py +5 -8
- package/bin/_lib_fmt.py +325 -0
- package/bin/_lib_render.py +9 -24
- package/bin/cctally +33 -273
- package/dashboard/static/assets/index-CXZDQrV3.js +18 -0
- package/dashboard/static/assets/{index-BxmaYT1y.css → index-ZHOC14y-.css} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +3 -1
- package/dashboard/static/assets/index-CLcd-Tnm.js +0 -18
package/bin/cctally
CHANGED
|
@@ -66,7 +66,6 @@ import textwrap
|
|
|
66
66
|
import threading
|
|
67
67
|
import time
|
|
68
68
|
import traceback
|
|
69
|
-
import unicodedata
|
|
70
69
|
import urllib.error
|
|
71
70
|
import urllib.request
|
|
72
71
|
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
|
@@ -329,6 +328,22 @@ format_display_dt = _lib_display_tz.format_display_dt
|
|
|
329
328
|
_argparse_tz = _lib_display_tz._argparse_tz
|
|
330
329
|
|
|
331
330
|
|
|
331
|
+
# fmt/color/table primitives moved to _lib_fmt.py (#126 C11)
|
|
332
|
+
_lib_fmt = _load_sibling("_lib_fmt")
|
|
333
|
+
_parse_iso_datetime_optional = _lib_fmt._parse_iso_datetime_optional
|
|
334
|
+
_format_ts_compact = _lib_fmt._format_ts_compact
|
|
335
|
+
_format_week_window = _lib_fmt._format_week_window
|
|
336
|
+
_supports_color_stdout = _lib_fmt._supports_color_stdout
|
|
337
|
+
_style_ansi = _lib_fmt._style_ansi
|
|
338
|
+
_supports_unicode_stdout = _lib_fmt._supports_unicode_stdout
|
|
339
|
+
_display_width = _lib_fmt._display_width
|
|
340
|
+
_boxed_table = _lib_fmt._boxed_table
|
|
341
|
+
_fmt_num = _lib_fmt._fmt_num
|
|
342
|
+
_truncate_num = _lib_fmt._truncate_num
|
|
343
|
+
_ANSI_ESC_RE = _lib_fmt._ANSI_ESC_RE
|
|
344
|
+
_truncate_display = _lib_fmt._truncate_display
|
|
345
|
+
|
|
346
|
+
|
|
332
347
|
_cctally_parser = _load_sibling("_cctally_parser")
|
|
333
348
|
build_parser = _cctally_parser.build_parser
|
|
334
349
|
_nonneg_int = _cctally_parser._nonneg_int
|
|
@@ -351,14 +366,22 @@ _build_codex_weekly_parser = _cctally_parser._build_codex_weekly_parser
|
|
|
351
366
|
_build_codex_session_parser = _cctally_parser._build_codex_session_parser
|
|
352
367
|
|
|
353
368
|
|
|
369
|
+
_lib_alert_axes = _load_sibling("_lib_alert_axes")
|
|
370
|
+
severity_for = _lib_alert_axes.severity_for
|
|
371
|
+
AlertAxisDescriptor = _lib_alert_axes.AlertAxisDescriptor
|
|
372
|
+
AXIS_REGISTRY = _lib_alert_axes.AXIS_REGISTRY
|
|
373
|
+
AXIS_BY_ID = _lib_alert_axes.AXIS_BY_ID
|
|
374
|
+
|
|
354
375
|
_lib_alerts_payload = _load_sibling("_lib_alerts_payload")
|
|
355
376
|
_alert_text_weekly = _lib_alerts_payload._alert_text_weekly
|
|
356
377
|
_alert_text_five_hour = _lib_alerts_payload._alert_text_five_hour
|
|
357
378
|
_alert_text_budget = _lib_alerts_payload._alert_text_budget
|
|
379
|
+
_alert_text_projected = _lib_alerts_payload._alert_text_projected
|
|
358
380
|
_escape_applescript_string = _lib_alerts_payload._escape_applescript_string
|
|
359
381
|
_build_alert_payload_weekly = _lib_alerts_payload._build_alert_payload_weekly
|
|
360
382
|
_build_alert_payload_five_hour = _lib_alerts_payload._build_alert_payload_five_hour
|
|
361
383
|
_build_alert_payload_budget = _lib_alerts_payload._build_alert_payload_budget
|
|
384
|
+
_build_alert_payload_projected = _lib_alerts_payload._build_alert_payload_projected
|
|
362
385
|
|
|
363
386
|
_lib_five_hour = _load_sibling("_lib_five_hour")
|
|
364
387
|
_FIVE_HOUR_JITTER_FLOOR_SECONDS = _lib_five_hour._FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
@@ -784,6 +807,8 @@ _PERCENT_NORMALIZE_DECIMALS = _cctally_record._PERCENT_NORMALIZE_DECIMALS
|
|
|
784
807
|
_normalize_percent = _cctally_record._normalize_percent
|
|
785
808
|
maybe_record_milestone = _cctally_record.maybe_record_milestone
|
|
786
809
|
maybe_record_budget_milestone = _cctally_record.maybe_record_budget_milestone
|
|
810
|
+
maybe_record_projected_alert = _cctally_record.maybe_record_projected_alert
|
|
811
|
+
_weekly_pct_week_avg_projection = _cctally_record._weekly_pct_week_avg_projection
|
|
787
812
|
_compute_block_totals = _cctally_record._compute_block_totals
|
|
788
813
|
maybe_update_five_hour_block = _cctally_record.maybe_update_five_hour_block
|
|
789
814
|
cmd_record_usage = _cctally_record.cmd_record_usage
|
|
@@ -1920,75 +1945,8 @@ ORIGINAL_ENTRYPOINT: "str | None" = None
|
|
|
1920
1945
|
_UPDATE_WORKER: "UpdateWorker | None" = None
|
|
1921
1946
|
|
|
1922
1947
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
return None
|
|
1926
|
-
try:
|
|
1927
|
-
return parse_iso_datetime(value, "timestamp")
|
|
1928
|
-
except ValueError:
|
|
1929
|
-
return None
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
def _format_ts_compact(
|
|
1933
|
-
value: str | None,
|
|
1934
|
-
tz: "ZoneInfo | None" = None,
|
|
1935
|
-
) -> str:
|
|
1936
|
-
"""Compact ISO-instant -> "YYYY-MM-DD HH:MM" line.
|
|
1937
|
-
|
|
1938
|
-
F5 fix: optional ``tz`` localizes the parsed instant before strftime
|
|
1939
|
-
AND appends the offset suffix via ``display_tz_label`` so the column
|
|
1940
|
-
becomes unambiguous (mirrors ``format_display_dt``'s pattern). When
|
|
1941
|
-
``tz`` is None, returns the legacy UTC-clock string with no suffix
|
|
1942
|
-
so existing callers keep their byte-stable output.
|
|
1943
|
-
"""
|
|
1944
|
-
parsed = _parse_iso_datetime_optional(value)
|
|
1945
|
-
if parsed is None:
|
|
1946
|
-
return "n/a"
|
|
1947
|
-
if tz is None:
|
|
1948
|
-
# Host-local fallback / default-config path: preserve the original
|
|
1949
|
-
# byte-stable host-naive strftime output (UTC-aware datetimes render
|
|
1950
|
-
# UTC clock, no suffix). This branch is reachable in production for
|
|
1951
|
-
# users whose ``display.tz`` resolves to None — NOT a legacy or
|
|
1952
|
-
# back-compat path. The non-None branch routes through
|
|
1953
|
-
# ``format_display_dt`` for tz-aware rendering.
|
|
1954
|
-
return parsed.strftime("%Y-%m-%d %H:%M")
|
|
1955
|
-
return format_display_dt(parsed, tz, fmt="%Y-%m-%d %H:%M", suffix=True)
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
def _format_week_window(
|
|
1959
|
-
week_start_date: str | None,
|
|
1960
|
-
week_end_date: str | None,
|
|
1961
|
-
week_start_at: str | None,
|
|
1962
|
-
week_end_at: str | None,
|
|
1963
|
-
tz: "ZoneInfo | None" = None,
|
|
1964
|
-
) -> str:
|
|
1965
|
-
"""Render a "<start> -> <end>" week-window column. F5 adds tz-aware
|
|
1966
|
-
rendering for ISO-timestamp-bearing rows; legacy date-only rows pass
|
|
1967
|
-
through unchanged. ``tz=None`` preserves byte-stable callers."""
|
|
1968
|
-
if week_start_at and week_end_at:
|
|
1969
|
-
return (
|
|
1970
|
-
f"{_format_ts_compact(week_start_at, tz=tz)} -> "
|
|
1971
|
-
f"{_format_ts_compact(week_end_at, tz=tz)}"
|
|
1972
|
-
)
|
|
1973
|
-
return f"{week_start_date or 'n/a'} -> {week_end_date or 'n/a'}"
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
def _supports_color_stdout() -> bool:
|
|
1977
|
-
# Matches ccusage's picocolors behavior exactly.
|
|
1978
|
-
# FORCE_COLOR always enables (any value, including empty)
|
|
1979
|
-
if "FORCE_COLOR" in os.environ:
|
|
1980
|
-
return True
|
|
1981
|
-
# NO_COLOR always disables (any value, including empty)
|
|
1982
|
-
if "NO_COLOR" in os.environ:
|
|
1983
|
-
return False
|
|
1984
|
-
# CI environments get color
|
|
1985
|
-
if "CI" in os.environ:
|
|
1986
|
-
return True
|
|
1987
|
-
# TTY check on stdout or stderr
|
|
1988
|
-
if sys.stdout.isatty() or sys.stderr.isatty():
|
|
1989
|
-
term = os.environ.get("TERM", "")
|
|
1990
|
-
return term.lower() != "dumb"
|
|
1991
|
-
return False
|
|
1948
|
+
# fmt/color/table primitives + _parse_iso_datetime_optional now live in
|
|
1949
|
+
# _lib_fmt.py (re-exported above) — #126 C11.
|
|
1992
1950
|
|
|
1993
1951
|
|
|
1994
1952
|
def _resolve_color_enabled(args: argparse.Namespace) -> bool:
|
|
@@ -2049,209 +2007,6 @@ def _resolve_color_enabled(args: argparse.Namespace) -> bool:
|
|
|
2049
2007
|
return False
|
|
2050
2008
|
|
|
2051
2009
|
|
|
2052
|
-
def _style_ansi(text: str, code: str, enabled: bool) -> str:
|
|
2053
|
-
if not enabled:
|
|
2054
|
-
return text
|
|
2055
|
-
return f"\033[{code}m{text}\033[0m"
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
def _supports_unicode_stdout() -> bool:
|
|
2059
|
-
encoding = (sys.stdout.encoding or "").upper()
|
|
2060
|
-
return "UTF" in encoding
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
def _display_width(s: str) -> int:
|
|
2064
|
-
"""Terminal cells consumed by ``s``.
|
|
2065
|
-
|
|
2066
|
-
Counts each codepoint by its East Asian Width: ``W`` / ``F`` (Wide
|
|
2067
|
-
/ Fullwidth) → 2 cells; combining marks → 0; everything else → 1.
|
|
2068
|
-
Ambiguous (``A``) defaults to 1, matching every non-CJK terminal
|
|
2069
|
-
locale — cctally has no CJK content in cell data, and `→` / `—` /
|
|
2070
|
-
`·` (all `A`) are intentionally rendered narrow.
|
|
2071
|
-
|
|
2072
|
-
Used by `_boxed_table` so cells containing wide glyphs (notably
|
|
2073
|
-
`⚡` U+26A1 on credit-row annotations) pad to the right cell count
|
|
2074
|
-
rather than the right codepoint count. Without this, `len()`-based
|
|
2075
|
-
padding under-pads by one cell per wide glyph and the right border
|
|
2076
|
-
drifts off-column on those rows only.
|
|
2077
|
-
"""
|
|
2078
|
-
width = 0
|
|
2079
|
-
for ch in s:
|
|
2080
|
-
if unicodedata.combining(ch):
|
|
2081
|
-
continue
|
|
2082
|
-
width += 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
|
|
2083
|
-
return width
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
def _boxed_table(
|
|
2087
|
-
headers: list[str],
|
|
2088
|
-
rows: list[list[str]],
|
|
2089
|
-
aligns: list[str] | None = None,
|
|
2090
|
-
*,
|
|
2091
|
-
color_header: bool = True,
|
|
2092
|
-
compact: bool = False,
|
|
2093
|
-
) -> str:
|
|
2094
|
-
if not headers:
|
|
2095
|
-
return ""
|
|
2096
|
-
col_count = len(headers)
|
|
2097
|
-
aligns = aligns or ["left"] * col_count
|
|
2098
|
-
if len(aligns) != col_count:
|
|
2099
|
-
raise ValueError("aligns length must match headers length")
|
|
2100
|
-
|
|
2101
|
-
sanitized_rows: list[list[str]] = []
|
|
2102
|
-
for row in rows:
|
|
2103
|
-
normalized = [str(cell).replace("\n", " ") for cell in row]
|
|
2104
|
-
if len(normalized) != col_count:
|
|
2105
|
-
raise ValueError("row length must match headers length")
|
|
2106
|
-
sanitized_rows.append(normalized)
|
|
2107
|
-
|
|
2108
|
-
widths: list[int] = []
|
|
2109
|
-
for idx, header in enumerate(headers):
|
|
2110
|
-
max_cell = max((_display_width(r[idx]) for r in sanitized_rows), default=0)
|
|
2111
|
-
widths.append(max(_display_width(header), max_cell))
|
|
2112
|
-
|
|
2113
|
-
def _pad(text: str, width: int, align: str) -> str:
|
|
2114
|
-
deficit = width - _display_width(text)
|
|
2115
|
-
if deficit <= 0:
|
|
2116
|
-
return text
|
|
2117
|
-
pad = " " * deficit
|
|
2118
|
-
if align == "right":
|
|
2119
|
-
return pad + text
|
|
2120
|
-
if align == "center":
|
|
2121
|
-
left = deficit // 2
|
|
2122
|
-
return (" " * left) + text + (" " * (deficit - left))
|
|
2123
|
-
return text + pad
|
|
2124
|
-
|
|
2125
|
-
if _supports_unicode_stdout():
|
|
2126
|
-
chars = {
|
|
2127
|
-
"top_left": "┌",
|
|
2128
|
-
"top_mid": "┬",
|
|
2129
|
-
"top_right": "┐",
|
|
2130
|
-
"mid_left": "├",
|
|
2131
|
-
"mid_mid": "┼",
|
|
2132
|
-
"mid_right": "┤",
|
|
2133
|
-
"bottom_left": "└",
|
|
2134
|
-
"bottom_mid": "┴",
|
|
2135
|
-
"bottom_right": "┘",
|
|
2136
|
-
"h": "─",
|
|
2137
|
-
"v": "│",
|
|
2138
|
-
}
|
|
2139
|
-
else:
|
|
2140
|
-
chars = {
|
|
2141
|
-
"top_left": "+",
|
|
2142
|
-
"top_mid": "+",
|
|
2143
|
-
"top_right": "+",
|
|
2144
|
-
"mid_left": "+",
|
|
2145
|
-
"mid_mid": "+",
|
|
2146
|
-
"mid_right": "+",
|
|
2147
|
-
"bottom_left": "+",
|
|
2148
|
-
"bottom_mid": "+",
|
|
2149
|
-
"bottom_right": "+",
|
|
2150
|
-
"h": "-",
|
|
2151
|
-
"v": "|",
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
color_enabled = _supports_color_stdout()
|
|
2155
|
-
|
|
2156
|
-
def _dim(s: str) -> str:
|
|
2157
|
-
return _style_ansi(s, "90", color_enabled)
|
|
2158
|
-
|
|
2159
|
-
# Issue #91 (Shape B): ``compact`` drops the 1-space cell padding to
|
|
2160
|
-
# 0 on this content-sized table (which has no proportional-width path
|
|
2161
|
-
# to force). Borders and rows both key off ``pad`` so the default
|
|
2162
|
-
# (``pad == 1``) reproduces the prior output byte-for-byte.
|
|
2163
|
-
pad = 0 if compact else 1
|
|
2164
|
-
pad_s = " " * pad
|
|
2165
|
-
|
|
2166
|
-
def make_border(left: str, mid: str, right: str) -> str:
|
|
2167
|
-
return _dim(
|
|
2168
|
-
left
|
|
2169
|
-
+ mid.join(chars["h"] * (w + 2 * pad) for w in widths)
|
|
2170
|
-
+ right
|
|
2171
|
-
)
|
|
2172
|
-
|
|
2173
|
-
def make_row(cells: list[str], *, header: bool = False) -> str:
|
|
2174
|
-
is_total = not header and cells and cells[0].strip() == "Total"
|
|
2175
|
-
styled_cells: list[str] = []
|
|
2176
|
-
for i, raw in enumerate(cells):
|
|
2177
|
-
text = _pad(raw, widths[i], aligns[i])
|
|
2178
|
-
if header and color_header:
|
|
2179
|
-
text = _style_ansi(text, "36", color_enabled) # cyan text, like ccusage table head
|
|
2180
|
-
elif is_total:
|
|
2181
|
-
text = _style_ansi(text, "32", color_enabled) # green text for totals
|
|
2182
|
-
styled_cells.append(text)
|
|
2183
|
-
v = _dim(chars["v"])
|
|
2184
|
-
return (
|
|
2185
|
-
v
|
|
2186
|
-
+ pad_s
|
|
2187
|
-
+ f"{pad_s}{v}{pad_s}".join(styled_cells)
|
|
2188
|
-
+ pad_s
|
|
2189
|
-
+ v
|
|
2190
|
-
)
|
|
2191
|
-
|
|
2192
|
-
top = make_border(chars["top_left"], chars["top_mid"], chars["top_right"])
|
|
2193
|
-
mid = make_border(chars["mid_left"], chars["mid_mid"], chars["mid_right"])
|
|
2194
|
-
bottom = make_border(chars["bottom_left"], chars["bottom_mid"], chars["bottom_right"])
|
|
2195
|
-
|
|
2196
|
-
out_lines = [top, make_row(headers, header=True), mid]
|
|
2197
|
-
for idx, row in enumerate(sanitized_rows):
|
|
2198
|
-
out_lines.append(make_row(row, header=False))
|
|
2199
|
-
if idx < len(sanitized_rows) - 1:
|
|
2200
|
-
out_lines.append(mid)
|
|
2201
|
-
out_lines.append(bottom)
|
|
2202
|
-
return "\n".join(out_lines)
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
def _fmt_num(n: int) -> str:
|
|
2206
|
-
"""Format integer with comma separators: 1234567 -> '1,234,567'."""
|
|
2207
|
-
return f"{n:,}"
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
def _truncate_num(formatted: str, width: int) -> str:
|
|
2211
|
-
"""Truncate a formatted number to fit width, replacing tail with '…'."""
|
|
2212
|
-
if len(formatted) <= width:
|
|
2213
|
-
return formatted
|
|
2214
|
-
return formatted[: width - 1] + "\u2026"
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
_ANSI_ESC_RE = re.compile(r"\033\[[0-9;]*m")
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
def _truncate_display(text: str, width: int) -> str:
|
|
2221
|
-
"""Truncate to `width` visible chars, preserving ANSI escape sequences.
|
|
2222
|
-
|
|
2223
|
-
Unlike `_truncate_num`, which slices raw string indices, this walks
|
|
2224
|
-
the text treating `\\033[...m` sequences as zero-width and counts
|
|
2225
|
-
printable chars toward the width budget. Used for left-aligned
|
|
2226
|
-
cells that may carry a styled anomaly-glyph prefix — slicing those
|
|
2227
|
-
with `_truncate_num` can cut through an ANSI escape and bleed
|
|
2228
|
-
color into adjacent cells.
|
|
2229
|
-
"""
|
|
2230
|
-
# Fast path: no ANSI codes, fall back to raw-slice truncation.
|
|
2231
|
-
if "\033" not in text:
|
|
2232
|
-
return _truncate_num(text, width)
|
|
2233
|
-
stripped_len = len(_ANSI_ESC_RE.sub("", text))
|
|
2234
|
-
if stripped_len <= width:
|
|
2235
|
-
return text
|
|
2236
|
-
# Walk chars until we've emitted (width - 1) visible chars, copying
|
|
2237
|
-
# ANSI sequences verbatim. Append reset + ellipsis to close any open
|
|
2238
|
-
# style and preserve the fit-to-width contract.
|
|
2239
|
-
out: list[str] = []
|
|
2240
|
-
visible = 0
|
|
2241
|
-
i = 0
|
|
2242
|
-
target = width - 1
|
|
2243
|
-
while i < len(text) and visible < target:
|
|
2244
|
-
m = _ANSI_ESC_RE.match(text, i)
|
|
2245
|
-
if m:
|
|
2246
|
-
out.append(m.group(0))
|
|
2247
|
-
i = m.end()
|
|
2248
|
-
continue
|
|
2249
|
-
out.append(text[i])
|
|
2250
|
-
visible += 1
|
|
2251
|
-
i += 1
|
|
2252
|
-
return "".join(out) + "\033[0m\u2026"
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
2010
|
def _trend_row_recency_seconds(row: dict[str, Any]) -> float:
|
|
2256
2011
|
for key in ("asOf", "costCapturedAt", "usageCapturedAt", "weekStartAt"):
|
|
2257
2012
|
parsed = _parse_iso_datetime_optional(row.get(key))
|
|
@@ -2308,6 +2063,8 @@ get_milestone_cost_for_week = _cctally_milestones.get_milestone_cost_for
|
|
|
2308
2063
|
get_milestones_for_week = _cctally_milestones.get_milestones_for_week # forecast c.; tui shim; percent-breakdown c.
|
|
2309
2064
|
insert_percent_milestone = _cctally_milestones.insert_percent_milestone # record shim; idempotency-test mod.
|
|
2310
2065
|
insert_budget_milestone = _cctally_milestones.insert_budget_milestone # record shim
|
|
2066
|
+
insert_projected_milestone = _cctally_milestones.insert_projected_milestone # record shim
|
|
2067
|
+
_projected_levels_already_latched = _cctally_milestones._projected_levels_already_latched # record shim
|
|
2311
2068
|
_reconcile_budget_milestones_on_set = _cctally_milestones._reconcile_budget_milestones_on_set # test_budget_alerts ns[]
|
|
2312
2069
|
_reconcile_budget_on_config_write = _cctally_milestones._reconcile_budget_on_config_write # forecast/config/dashboard c.; test_forecast_ns_patch mod. patch
|
|
2313
2070
|
|
|
@@ -2650,6 +2407,9 @@ _compute_cost_for_weekref = _cctally_weekrefs._compute_cost_for_weekre
|
|
|
2650
2407
|
_apply_overlap_clamp_to_weekrefs = _cctally_weekrefs._apply_overlap_clamp_to_weekrefs
|
|
2651
2408
|
_RESET_PCT_DROP_THRESHOLD = _cctally_weekrefs._RESET_PCT_DROP_THRESHOLD
|
|
2652
2409
|
_FIVE_HOUR_RESET_PCT_DROP_THRESHOLD = _cctally_weekrefs._FIVE_HOUR_RESET_PCT_DROP_THRESHOLD
|
|
2410
|
+
_RESET_ZERO_FLOOR_PCT = _cctally_weekrefs._RESET_ZERO_FLOOR_PCT
|
|
2411
|
+
_RESET_ZERO_MIN_DROP_PCT = _cctally_weekrefs._RESET_ZERO_MIN_DROP_PCT
|
|
2412
|
+
_is_reset_drop = _cctally_weekrefs._is_reset_drop
|
|
2653
2413
|
|
|
2654
2414
|
|
|
2655
2415
|
# Eager re-export of bin/_cctally_percent_breakdown.py — the percent-breakdown
|