vishnu-standard-time 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.
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: vishnu-standard-time
3
+ Version: 0.2.0
4
+ Summary: Vishnu Standard Time: a flow-based productivity clock for humans and code.
5
+ Author: Vishnu Gottimukkala
6
+ License: MIT
7
+ Project-URL: Homepage, https://time.vishnu.store
8
+ Project-URL: Documentation, https://time.vishnu.store
9
+ Project-URL: Source, https://time.vishnu.store
10
+ Project-URL: Changelog, https://time.vishnu.store/.well-known/vst.json
11
+ Keywords: time,clock,productivity,cli,vst,vishnu-standard-time
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Office/Business :: Scheduling
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Topic :: Utilities
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Vishnu Standard Time for Python
30
+
31
+ Vishnu Standard Time is a flow-based productivity clock for humans and code.
32
+
33
+ Official clock:
34
+
35
+ https://time.vishnu.store
36
+
37
+ ```bash
38
+ pip install vishnu-standard-time
39
+ ```
40
+
41
+ ```python
42
+ from vishnu_standard_time import local_now, format_vst, flows_for
43
+
44
+ parts = local_now()
45
+ print(format_vst(parts))
46
+ print(flows_for(minutes=25)) # 4500
47
+ ```
48
+
49
+ CLI:
50
+
51
+ ```bash
52
+ vst now
53
+ vst now --json
54
+ vst percent
55
+ vst flows-left
56
+ vst stamp "started deploy"
57
+ vst convert 25m
58
+ vst after 4500
59
+ vst until 180000
60
+ vst timer 25m "deep work"
61
+ vst focus 25m "write README"
62
+ vst prompt
63
+ vst badge vst.svg
64
+ vst link
65
+ ```
66
+
67
+ Core units:
68
+
69
+ ```text
70
+ 1 day = 259,200 flows
71
+ 1 minute = 180 flows
72
+ 1 second = 3 flows
73
+ 1 flow = 333⅓ ms
74
+ ```
75
+
76
+ Vishnu Standard Time — https://time.vishnu.store
@@ -0,0 +1,48 @@
1
+ # Vishnu Standard Time for Python
2
+
3
+ Vishnu Standard Time is a flow-based productivity clock for humans and code.
4
+
5
+ Official clock:
6
+
7
+ https://time.vishnu.store
8
+
9
+ ```bash
10
+ pip install vishnu-standard-time
11
+ ```
12
+
13
+ ```python
14
+ from vishnu_standard_time import local_now, format_vst, flows_for
15
+
16
+ parts = local_now()
17
+ print(format_vst(parts))
18
+ print(flows_for(minutes=25)) # 4500
19
+ ```
20
+
21
+ CLI:
22
+
23
+ ```bash
24
+ vst now
25
+ vst now --json
26
+ vst percent
27
+ vst flows-left
28
+ vst stamp "started deploy"
29
+ vst convert 25m
30
+ vst after 4500
31
+ vst until 180000
32
+ vst timer 25m "deep work"
33
+ vst focus 25m "write README"
34
+ vst prompt
35
+ vst badge vst.svg
36
+ vst link
37
+ ```
38
+
39
+ Core units:
40
+
41
+ ```text
42
+ 1 day = 259,200 flows
43
+ 1 minute = 180 flows
44
+ 1 second = 3 flows
45
+ 1 flow = 333⅓ ms
46
+ ```
47
+
48
+ Vishnu Standard Time — https://time.vishnu.store
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vishnu-standard-time"
7
+ version = "0.2.0"
8
+ description = "Vishnu Standard Time: a flow-based productivity clock for humans and code."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Vishnu Gottimukkala" }
14
+ ]
15
+ keywords = ["time", "clock", "productivity", "cli", "vst", "vishnu-standard-time"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Office/Business :: Scheduling",
29
+ "Topic :: Software Development :: Libraries :: Python Modules",
30
+ "Topic :: Utilities"
31
+ ]
32
+
33
+ dependencies = []
34
+
35
+ [project.urls]
36
+ Homepage = "https://time.vishnu.store"
37
+ Documentation = "https://time.vishnu.store"
38
+ Source = "https://time.vishnu.store"
39
+ Changelog = "https://time.vishnu.store/.well-known/vst.json"
40
+
41
+ [project.scripts]
42
+ vst = "vishnu_standard_time.cli:main"
43
+ vst-py = "vishnu_standard_time.cli:main"
44
+
45
+ [tool.setuptools]
46
+ package-dir = {"" = "src"}
47
+
48
+ [tool.setuptools.packages.find]
49
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,371 @@
1
+ """Vishnu Standard Time: flow-based productivity time for humans and code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from html import escape
7
+ import json
8
+ import math
9
+ import time
10
+ from typing import Any, Callable, Dict, List, Optional
11
+
12
+ VERSION = "0.2.0"
13
+ SCALE = "VST"
14
+ STANDARD_NAME = "Vishnu Standard Time"
15
+ WEBSITE_URL = "https://time.vishnu.store"
16
+ STANDARD_LINK = f"{STANDARD_NAME} — {WEBSITE_URL}"
17
+
18
+ FLOWS_PER_SECOND = 3
19
+ FLOWS_PER_MINUTE = 180
20
+ FLOWS_PER_HOUR = 10_800
21
+ FLOWS_PER_DAY = 259_200
22
+ MS_PER_DAY = 86_400_000
23
+ # Hidden machine anchor for interop only: 2026-01-01T00:00:00.000Z
24
+ EPOCH_UNIX_MS = 1_767_225_600_000
25
+
26
+ # Legacy vick/pulse compatibility from early VST versions.
27
+ VICKS_PER_DAY = 100_000
28
+ PULSES_PER_VICK = 1_000
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class VstParts:
33
+ day: int
34
+ flow: int
35
+ flow_of_minute: int
36
+ day_ms: int
37
+ unix_ms: int
38
+ vick: int
39
+ pulse: int
40
+
41
+ @property
42
+ def flows_left_today(self) -> int:
43
+ return flows_left_today(self)
44
+
45
+ @property
46
+ def percent_of_day(self) -> float:
47
+ return percent_of_day(self)
48
+
49
+ @property
50
+ def percent_remaining(self) -> float:
51
+ return percent_remaining(self)
52
+
53
+ @property
54
+ def text(self) -> str:
55
+ return format_vst(self)
56
+
57
+ @property
58
+ def precise_text(self) -> str:
59
+ return format_precise(self)
60
+
61
+ def to_dict(self) -> Dict[str, Any]:
62
+ return to_dict(self)
63
+
64
+
65
+ def unix_ms_now() -> int:
66
+ return time.time_ns() // 1_000_000
67
+
68
+
69
+ def from_unix_ms(unix_ms: int) -> VstParts:
70
+ elapsed = int(unix_ms) - EPOCH_UNIX_MS
71
+ day = math.floor(elapsed / MS_PER_DAY)
72
+ day_ms = elapsed - day * MS_PER_DAY
73
+ flow = min(FLOWS_PER_DAY - 1, max(0, (day_ms * FLOWS_PER_DAY) // MS_PER_DAY))
74
+ flow_of_minute = flow % FLOWS_PER_MINUTE
75
+ vick = min(VICKS_PER_DAY - 1, max(0, (day_ms * VICKS_PER_DAY) // MS_PER_DAY))
76
+ vick_start_ms = (vick * MS_PER_DAY) // VICKS_PER_DAY
77
+ pulse = min(PULSES_PER_VICK - 1, max(0, ((day_ms - vick_start_ms) * PULSES_PER_VICK) // (MS_PER_DAY // VICKS_PER_DAY or 1)))
78
+ return VstParts(day=int(day), flow=int(flow), flow_of_minute=int(flow_of_minute), day_ms=int(day_ms), unix_ms=int(unix_ms), vick=int(vick), pulse=int(pulse))
79
+
80
+
81
+ def local_now() -> VstParts:
82
+ return from_unix_ms(unix_ms_now())
83
+
84
+
85
+ def now() -> VstParts:
86
+ return local_now()
87
+
88
+
89
+ def at_day_flow(day: int, flow: int) -> VstParts:
90
+ flow = int(flow) % FLOWS_PER_DAY
91
+ unix_ms = EPOCH_UNIX_MS + int(day) * MS_PER_DAY + (flow * MS_PER_DAY) // FLOWS_PER_DAY
92
+ return from_unix_ms(unix_ms)
93
+
94
+
95
+ def format_int(value: int) -> str:
96
+ return f"{int(value):,}"
97
+
98
+
99
+ def format_day(day: int) -> str:
100
+ sign = "+" if int(day) >= 0 else "-"
101
+ return f"D{sign}{abs(int(day)):06d}"
102
+
103
+
104
+ def percent_of_day(parts: Optional[VstParts] = None) -> float:
105
+ p = parts or local_now()
106
+ return (p.day_ms / MS_PER_DAY) * 100.0
107
+
108
+
109
+ def percent_remaining(parts: Optional[VstParts] = None) -> float:
110
+ return 100.0 - percent_of_day(parts)
111
+
112
+
113
+ def flows_left_today(parts: Optional[VstParts] = None) -> int:
114
+ p = parts or local_now()
115
+ return FLOWS_PER_DAY - p.flow
116
+
117
+
118
+ def format_percent_of_day(parts: Optional[VstParts] = None) -> str:
119
+ return f"{percent_of_day(parts):05.2f}% OF DAY"
120
+
121
+
122
+ def format_percent_remaining(parts: Optional[VstParts] = None) -> str:
123
+ return f"{percent_remaining(parts):05.2f}% DAY REMAINING"
124
+
125
+
126
+ def format_flow_count(parts: Optional[VstParts] = None) -> str:
127
+ p = parts or local_now()
128
+ return f"{format_int(p.flow)} / {format_int(FLOWS_PER_DAY)} flows"
129
+
130
+
131
+ def format_flow_of_minute(parts: Optional[VstParts] = None) -> str:
132
+ p = parts or local_now()
133
+ return f"FLOW {p.flow_of_minute:03d} / {FLOWS_PER_MINUTE - 1}"
134
+
135
+
136
+ def format_vst(parts: Optional[VstParts] = None) -> str:
137
+ p = parts or local_now()
138
+ return f"VST {format_day(p.day)} · {format_flow_count(p)} · {format_percent_of_day(p)}"
139
+
140
+
141
+ def format_precise(parts: Optional[VstParts] = None) -> str:
142
+ p = parts or local_now()
143
+ return f"VST {format_day(p.day)} · {format_flow_of_minute(p)} · {format_flow_count(p)} · {format_percent_of_day(p)}"
144
+
145
+
146
+ def format_flows_left(parts: Optional[VstParts] = None) -> str:
147
+ return f"{format_int(flows_left_today(parts))} flows left today"
148
+
149
+
150
+ def prompt(parts: Optional[VstParts] = None) -> str:
151
+ p = parts or local_now()
152
+ return f"VST {format_int(p.flow)}/{format_int(FLOWS_PER_DAY)} {percent_of_day(p):.2f}%"
153
+
154
+
155
+ def website_url() -> str:
156
+ return WEBSITE_URL
157
+
158
+
159
+ def standard_link() -> str:
160
+ return STANDARD_LINK
161
+
162
+
163
+ def to_dict(parts: Optional[VstParts] = None) -> Dict[str, Any]:
164
+ p = parts or local_now()
165
+ return {
166
+ "scale": SCALE,
167
+ "version": VERSION,
168
+ "day": str(p.day),
169
+ "flow": p.flow,
170
+ "flow_of_minute": p.flow_of_minute,
171
+ "flows_per_day": FLOWS_PER_DAY,
172
+ "flows_left_today": flows_left_today(p),
173
+ "percent_of_day": round(percent_of_day(p), 6),
174
+ "percent_remaining": round(percent_remaining(p), 6),
175
+ "text": format_vst(p),
176
+ "precise_text": format_precise(p),
177
+ "day_ms": str(p.day_ms),
178
+ "vick": p.vick,
179
+ "pulse": p.pulse,
180
+ "website_url": WEBSITE_URL,
181
+ "standard_link": STANDARD_LINK,
182
+ }
183
+
184
+
185
+ def to_json(parts: Optional[VstParts] = None, *, indent: Optional[int] = 2) -> str:
186
+ return json.dumps(to_dict(parts), indent=indent)
187
+
188
+
189
+ def stamp(message: str, parts: Optional[VstParts] = None) -> str:
190
+ return f"[{format_vst(parts or local_now())}] {message}"
191
+
192
+
193
+ def flows_for(*, seconds: Optional[float] = None, minutes: Optional[float] = None, hours: Optional[float] = None, milliseconds: Optional[float] = None) -> int:
194
+ total_seconds = 0.0
195
+ if milliseconds is not None:
196
+ total_seconds += float(milliseconds) / 1000.0
197
+ if seconds is not None:
198
+ total_seconds += float(seconds)
199
+ if minutes is not None:
200
+ total_seconds += float(minutes) * 60.0
201
+ if hours is not None:
202
+ total_seconds += float(hours) * 3600.0
203
+ return int(round(total_seconds * FLOWS_PER_SECOND))
204
+
205
+
206
+ def seconds_for(*, flows: int) -> float:
207
+ return int(flows) / FLOWS_PER_SECOND
208
+
209
+
210
+ def minutes_for(*, flows: int) -> float:
211
+ return seconds_for(flows=flows) / 60.0
212
+
213
+
214
+ def hours_for(*, flows: int) -> float:
215
+ return minutes_for(flows=flows) / 60.0
216
+
217
+
218
+ def milliseconds_for(*, flows: int) -> int:
219
+ return int(round(seconds_for(flows=flows) * 1000.0))
220
+
221
+
222
+ def parse_duration(value: Any) -> int:
223
+ if isinstance(value, int):
224
+ return value
225
+ text = str(value).strip().lower().replace("_", "")
226
+ if not text:
227
+ raise ValueError("missing duration")
228
+ if text.endswith("ms"):
229
+ return flows_for(milliseconds=float(text[:-2]))
230
+ if text.endswith("s"):
231
+ return flows_for(seconds=float(text[:-1]))
232
+ if text.endswith("m"):
233
+ return flows_for(minutes=float(text[:-1]))
234
+ if text.endswith("h"):
235
+ return flows_for(hours=float(text[:-1]))
236
+ return int(text)
237
+
238
+
239
+ def duration_dict(flows: int, target: Optional[VstParts] = None) -> Dict[str, Any]:
240
+ return {
241
+ "flows": int(flows),
242
+ "milliseconds": milliseconds_for(flows=int(flows)),
243
+ "seconds": seconds_for(flows=int(flows)),
244
+ "minutes": minutes_for(flows=int(flows)),
245
+ "hours": hours_for(flows=int(flows)),
246
+ "target": to_dict(target) if target else None,
247
+ }
248
+
249
+
250
+ def after(flows: int, parts: Optional[VstParts] = None) -> VstParts:
251
+ p = parts or local_now()
252
+ return from_unix_ms(p.unix_ms + milliseconds_for(flows=int(flows)))
253
+
254
+
255
+ def until_flow(target_flow: int, parts: Optional[VstParts] = None) -> Dict[str, Any]:
256
+ p = parts or local_now()
257
+ target_flow = int(target_flow) % FLOWS_PER_DAY
258
+ day = p.day if target_flow > p.flow else p.day + 1
259
+ target = at_day_flow(day, target_flow)
260
+ flows = max(0, round((target.unix_ms - p.unix_ms) / 1000.0 * FLOWS_PER_SECOND))
261
+ return duration_dict(flows, target)
262
+
263
+
264
+ @dataclass(frozen=True)
265
+ class FocusSession:
266
+ label: str
267
+ flows: int
268
+ started_at: VstParts
269
+ ends_at: VstParts
270
+ mode: str = "FOCUS"
271
+ now_fn: Callable[[], VstParts] = local_now
272
+
273
+ @property
274
+ def elapsed_flows(self) -> int:
275
+ now_p = self.now_fn()
276
+ elapsed_ms = max(0, now_p.unix_ms - self.started_at.unix_ms)
277
+ return min(self.flows, int(round((elapsed_ms / 1000.0) * FLOWS_PER_SECOND)))
278
+
279
+ @property
280
+ def flows_remaining(self) -> int:
281
+ return max(0, self.flows - self.elapsed_flows)
282
+
283
+ @property
284
+ def percent_complete(self) -> float:
285
+ if self.flows <= 0:
286
+ return 100.0
287
+ return min(100.0, (self.elapsed_flows / self.flows) * 100.0)
288
+
289
+ def done(self) -> bool:
290
+ return self.flows_remaining <= 0
291
+
292
+ def summary(self) -> str:
293
+ return "\n".join([
294
+ f"{self.mode} · {self.label}",
295
+ f"Ends at: {format_vst(self.ends_at)}",
296
+ f"{format_int(self.flows_remaining)} flows left · {self.percent_complete:.2f}% complete",
297
+ ])
298
+
299
+
300
+ def focus(*, flows: Optional[int] = None, seconds: Optional[float] = None, minutes: Optional[float] = None, hours: Optional[float] = None, label: str = "deep work", mode: str = "FOCUS") -> FocusSession:
301
+ total = int(flows if flows is not None else flows_for(seconds=seconds, minutes=minutes, hours=hours))
302
+ started = local_now()
303
+ return FocusSession(label=label, flows=total, started_at=started, ends_at=after(total, started), mode=mode)
304
+
305
+
306
+ @dataclass(frozen=True)
307
+ class PlanBlock:
308
+ name: str
309
+ flows: int
310
+
311
+ @property
312
+ def seconds(self) -> float:
313
+ return seconds_for(flows=self.flows)
314
+
315
+ @property
316
+ def minutes(self) -> float:
317
+ return minutes_for(flows=self.flows)
318
+
319
+
320
+ class Plan:
321
+ def __init__(self) -> None:
322
+ self.blocks: List[PlanBlock] = []
323
+
324
+ def block(self, name: str, *, flows: Optional[int] = None, seconds: Optional[float] = None, minutes: Optional[float] = None, hours: Optional[float] = None) -> "Plan":
325
+ total = int(flows if flows is not None else flows_for(seconds=seconds, minutes=minutes, hours=hours))
326
+ self.blocks.append(PlanBlock(name=name, flows=total))
327
+ return self
328
+
329
+ @property
330
+ def total_flows(self) -> int:
331
+ return sum(b.flows for b in self.blocks)
332
+
333
+ @property
334
+ def percent_of_day(self) -> float:
335
+ return (self.total_flows / FLOWS_PER_DAY) * 100.0
336
+
337
+ def summary(self) -> str:
338
+ lines = [f"{b.name:<16} {format_int(b.flows):>9} flows · {b.minutes:.2f} min" for b in self.blocks]
339
+ lines.append("")
340
+ lines.append(f"Total: {format_int(self.total_flows)} flows · {self.percent_of_day:.2f}% of day")
341
+ return "\n".join(lines)
342
+
343
+
344
+ def plan(builder: Optional[Callable[[Plan], Any]] = None) -> Plan:
345
+ p = Plan()
346
+ if builder is not None:
347
+ builder(p)
348
+ return p
349
+
350
+
351
+ def svg_badge(parts: Optional[VstParts] = None) -> str:
352
+ p = parts or local_now()
353
+ label = escape(format_vst(p))
354
+ width = 560
355
+ return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="32" role="img" aria-label="{label}">
356
+ <rect width="{width}" height="32" rx="8" fill="#111"/>
357
+ <rect width="168" height="32" rx="8" fill="#222"/>
358
+ <text x="14" y="21" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13" fill="#fff">Vishnu Standard Time</text>
359
+ <text x="182" y="21" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13" fill="#fff">{label}</text>
360
+ </svg>
361
+ '''
362
+
363
+ __all__ = [
364
+ "VERSION", "SCALE", "STANDARD_NAME", "WEBSITE_URL", "STANDARD_LINK",
365
+ "FLOWS_PER_SECOND", "FLOWS_PER_MINUTE", "FLOWS_PER_HOUR", "FLOWS_PER_DAY", "EPOCH_UNIX_MS",
366
+ "VstParts", "local_now", "now", "from_unix_ms", "at_day_flow",
367
+ "format_vst", "format_precise", "format_flow_count", "format_percent_of_day", "format_percent_remaining", "format_flows_left", "prompt",
368
+ "website_url", "standard_link", "to_dict", "to_json", "stamp",
369
+ "flows_for", "seconds_for", "minutes_for", "hours_for", "milliseconds_for", "parse_duration", "duration_dict", "after", "until_flow",
370
+ "FocusSession", "focus", "Plan", "PlanBlock", "plan", "svg_badge",
371
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -0,0 +1,186 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ import time
6
+ from pathlib import Path
7
+
8
+ from . import (
9
+ VERSION,
10
+ STANDARD_LINK,
11
+ WEBSITE_URL,
12
+ duration_dict,
13
+ flows_left_today,
14
+ format_flows_left,
15
+ format_percent_of_day,
16
+ format_percent_remaining,
17
+ format_precise,
18
+ format_vst,
19
+ focus,
20
+ local_now,
21
+ parse_duration,
22
+ percent_of_day,
23
+ prompt,
24
+ stamp,
25
+ svg_badge,
26
+ to_json,
27
+ after,
28
+ until_flow,
29
+ )
30
+
31
+ HELP = f"""Vishnu Standard Time CLI
32
+
33
+ Usage:
34
+ vst now [--json] [--precise]
35
+ vst percent
36
+ vst flows-left
37
+ vst stamp MESSAGE
38
+ vst convert 4500|25m|90s|1h [--json]
39
+ vst after 4500|25m|90s|1h
40
+ vst until FLOW [--json]
41
+ vst timer 4500|25m [label]
42
+ vst focus 4500|25m [label]
43
+ vst prompt
44
+ vst badge [file.svg]
45
+ vst link
46
+
47
+ {STANDARD_LINK}
48
+ """
49
+
50
+
51
+ def _has(args, flag):
52
+ return flag in args
53
+
54
+
55
+ def _clean(args):
56
+ return [a for a in args if not a.startswith("--")]
57
+
58
+
59
+ def _print_duration(flows, target=None, as_json=False):
60
+ data = duration_dict(flows, target)
61
+ if as_json:
62
+ print(json.dumps(data, indent=2))
63
+ else:
64
+ print(f"{data['flows']:,} flows")
65
+ print(f"{data['seconds']:.2f} seconds")
66
+ print(f"{data['minutes']:.2f} minutes")
67
+
68
+
69
+ def _run_timer(args, mode):
70
+ if not args:
71
+ print(f"Missing duration for {mode.lower()}", file=sys.stderr)
72
+ return 1
73
+ flows = parse_duration(args[0])
74
+ label = " ".join(args[1:]) if len(args) > 1 else ("deep work" if mode == "FOCUS" else "timer")
75
+ session = focus(flows=flows, label=label, mode=mode)
76
+ print(session.summary())
77
+ # Lightweight real countdown for smoke tests and practical use.
78
+ sleep_s = min(max(flows / 3.0, 0.0), 1.0 if flows <= 3 else flows / 3.0)
79
+ if sleep_s > 0:
80
+ time.sleep(sleep_s)
81
+ print("Done.")
82
+ return 0
83
+
84
+
85
+ def call(argv=None):
86
+ args = list(sys.argv[1:] if argv is None else argv)
87
+ if not args or args[0] in {"help", "-h", "--help"}:
88
+ print(HELP)
89
+ return 0
90
+
91
+ cmd = args.pop(0)
92
+
93
+ if cmd in {"version", "--version", "-v"}:
94
+ print(VERSION)
95
+ return 0
96
+
97
+ if cmd == "now":
98
+ p = local_now()
99
+ if _has(args, "--json"):
100
+ print(to_json(p))
101
+ elif _has(args, "--precise"):
102
+ print(format_precise(p))
103
+ else:
104
+ print(format_vst(p))
105
+ return 0
106
+
107
+ if cmd == "percent":
108
+ p = local_now()
109
+ print(format_percent_of_day(p))
110
+ print(format_percent_remaining(p))
111
+ return 0
112
+
113
+ if cmd == "flows-left":
114
+ print(format_flows_left(local_now()))
115
+ return 0
116
+
117
+ if cmd == "stamp":
118
+ message = " ".join(args).strip()
119
+ if not message:
120
+ print("Missing stamp message", file=sys.stderr)
121
+ return 1
122
+ print(stamp(message))
123
+ return 0
124
+
125
+ if cmd == "convert":
126
+ clean = _clean(args)
127
+ if not clean:
128
+ print("Missing duration", file=sys.stderr)
129
+ return 1
130
+ flows = parse_duration(clean[0])
131
+ _print_duration(flows, as_json=_has(args, "--json"))
132
+ return 0
133
+
134
+ if cmd == "after":
135
+ clean = _clean(args)
136
+ if not clean:
137
+ print("Missing duration", file=sys.stderr)
138
+ return 1
139
+ flows = parse_duration(clean[0])
140
+ print(format_vst(after(flows)))
141
+ return 0
142
+
143
+ if cmd == "until":
144
+ clean = _clean(args)
145
+ if not clean:
146
+ print("Missing target flow", file=sys.stderr)
147
+ return 1
148
+ data = until_flow(int(clean[0].replace(",", "")))
149
+ if _has(args, "--json"):
150
+ print(json.dumps(data, indent=2))
151
+ else:
152
+ print(f"{data['flows']:,} flows until {clean[0]}")
153
+ print(format_vst(local_now() if data.get("target") is None else after(data["flows"])))
154
+ return 0
155
+
156
+ if cmd == "timer":
157
+ return _run_timer(args, "TIMER")
158
+
159
+ if cmd == "focus":
160
+ return _run_timer(args, "FOCUS")
161
+
162
+ if cmd == "prompt":
163
+ print(prompt())
164
+ return 0
165
+
166
+ if cmd == "badge":
167
+ file_name = args[0] if args else "vst.svg"
168
+ Path(file_name).write_text(svg_badge(), encoding="utf-8")
169
+ print(file_name)
170
+ return 0
171
+
172
+ if cmd == "link":
173
+ print(STANDARD_LINK)
174
+ return 0
175
+
176
+ print(f"Unknown command: {cmd}", file=sys.stderr)
177
+ print(HELP)
178
+ return 1
179
+
180
+
181
+ def main(argv=None):
182
+ raise SystemExit(call(argv))
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: vishnu-standard-time
3
+ Version: 0.2.0
4
+ Summary: Vishnu Standard Time: a flow-based productivity clock for humans and code.
5
+ Author: Vishnu Gottimukkala
6
+ License: MIT
7
+ Project-URL: Homepage, https://time.vishnu.store
8
+ Project-URL: Documentation, https://time.vishnu.store
9
+ Project-URL: Source, https://time.vishnu.store
10
+ Project-URL: Changelog, https://time.vishnu.store/.well-known/vst.json
11
+ Keywords: time,clock,productivity,cli,vst,vishnu-standard-time
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Office/Business :: Scheduling
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Topic :: Utilities
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Vishnu Standard Time for Python
30
+
31
+ Vishnu Standard Time is a flow-based productivity clock for humans and code.
32
+
33
+ Official clock:
34
+
35
+ https://time.vishnu.store
36
+
37
+ ```bash
38
+ pip install vishnu-standard-time
39
+ ```
40
+
41
+ ```python
42
+ from vishnu_standard_time import local_now, format_vst, flows_for
43
+
44
+ parts = local_now()
45
+ print(format_vst(parts))
46
+ print(flows_for(minutes=25)) # 4500
47
+ ```
48
+
49
+ CLI:
50
+
51
+ ```bash
52
+ vst now
53
+ vst now --json
54
+ vst percent
55
+ vst flows-left
56
+ vst stamp "started deploy"
57
+ vst convert 25m
58
+ vst after 4500
59
+ vst until 180000
60
+ vst timer 25m "deep work"
61
+ vst focus 25m "write README"
62
+ vst prompt
63
+ vst badge vst.svg
64
+ vst link
65
+ ```
66
+
67
+ Core units:
68
+
69
+ ```text
70
+ 1 day = 259,200 flows
71
+ 1 minute = 180 flows
72
+ 1 second = 3 flows
73
+ 1 flow = 333⅓ ms
74
+ ```
75
+
76
+ Vishnu Standard Time — https://time.vishnu.store
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/vishnu_standard_time/__init__.py
4
+ src/vishnu_standard_time/__main__.py
5
+ src/vishnu_standard_time/cli.py
6
+ src/vishnu_standard_time.egg-info/PKG-INFO
7
+ src/vishnu_standard_time.egg-info/SOURCES.txt
8
+ src/vishnu_standard_time.egg-info/dependency_links.txt
9
+ src/vishnu_standard_time.egg-info/entry_points.txt
10
+ src/vishnu_standard_time.egg-info/top_level.txt
11
+ tests/test_cli_smoke.py
12
+ tests/test_vishnu_standard_time.py
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ vst = vishnu_standard_time.cli:main
3
+ vst-py = vishnu_standard_time.cli:main
@@ -0,0 +1,59 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ import unittest
5
+ from pathlib import Path
6
+
7
+ ROOT = Path(__file__).resolve().parents[1]
8
+
9
+
10
+ def run_vst(*args):
11
+ return subprocess.run(
12
+ [sys.executable, "-m", "vishnu_standard_time", *args],
13
+ cwd=ROOT,
14
+ text=True,
15
+ stdout=subprocess.PIPE,
16
+ stderr=subprocess.PIPE,
17
+ env={**__import__("os").environ, "PYTHONPATH": str(ROOT / "src")},
18
+ )
19
+
20
+
21
+ class TestCliSmoke(unittest.TestCase):
22
+ def assert_ok(self, *args):
23
+ res = run_vst(*args)
24
+ self.assertEqual(res.returncode, 0, f"stdout={res.stdout}\nstderr={res.stderr}")
25
+ return res.stdout
26
+
27
+ def test_version(self):
28
+ self.assertIn("0.2.0", self.assert_ok("version"))
29
+
30
+ def test_prompt(self):
31
+ self.assertIn("VST", self.assert_ok("prompt"))
32
+
33
+ def test_link(self):
34
+ self.assertIn("https://time.vishnu.store", self.assert_ok("link"))
35
+
36
+ def test_badge(self):
37
+ f = ROOT / "tmp-vst-py-test.svg"
38
+ if f.exists():
39
+ f.unlink()
40
+ self.assert_ok("badge", str(f))
41
+ self.assertIn("<svg", f.read_text())
42
+ f.unlink()
43
+
44
+ def test_now_json(self):
45
+ data = json.loads(self.assert_ok("now", "--json"))
46
+ self.assertEqual(data["scale"], "VST")
47
+ self.assertEqual(data["website_url"], "https://time.vishnu.store")
48
+
49
+ def test_convert_json(self):
50
+ data = json.loads(self.assert_ok("convert", "25m", "--json"))
51
+ self.assertEqual(data["flows"], 4500)
52
+
53
+ def test_unknown(self):
54
+ res = run_vst("not-a-real-command")
55
+ self.assertEqual(res.returncode, 1)
56
+
57
+
58
+ if __name__ == "__main__":
59
+ unittest.main()
@@ -0,0 +1,54 @@
1
+ import json
2
+ import unittest
3
+
4
+ import vishnu_standard_time as vst
5
+
6
+
7
+ class TestVishnuStandardTime(unittest.TestCase):
8
+ def test_epoch_day_zero(self):
9
+ p = vst.from_unix_ms(vst.EPOCH_UNIX_MS)
10
+ self.assertEqual(p.day, 0)
11
+ self.assertEqual(p.flow, 0)
12
+ self.assertEqual(vst.format_vst(p), "VST D+000000 · 0 / 259,200 flows · 00.00% OF DAY")
13
+
14
+ def test_noon_is_half_day(self):
15
+ p = vst.from_unix_ms(vst.EPOCH_UNIX_MS + 43_200_000)
16
+ self.assertEqual(p.flow, 129_600)
17
+ self.assertAlmostEqual(vst.percent_of_day(p), 50.0)
18
+
19
+ def test_duration_helpers(self):
20
+ self.assertEqual(vst.flows_for(minutes=25), 4500)
21
+ self.assertEqual(vst.flows_for(seconds=90), 270)
22
+ self.assertEqual(vst.flows_for(hours=1), 10800)
23
+ self.assertEqual(vst.minutes_for(flows=900), 5.0)
24
+
25
+ def test_parse_duration(self):
26
+ self.assertEqual(vst.parse_duration("25m"), 4500)
27
+ self.assertEqual(vst.parse_duration("90s"), 270)
28
+ self.assertEqual(vst.parse_duration("1h"), 10800)
29
+ self.assertEqual(vst.parse_duration("4500"), 4500)
30
+
31
+ def test_to_dict_has_site(self):
32
+ d = vst.to_dict(vst.from_unix_ms(vst.EPOCH_UNIX_MS))
33
+ self.assertEqual(d["scale"], "VST")
34
+ self.assertEqual(d["website_url"], "https://time.vishnu.store")
35
+ self.assertIn("Vishnu Standard Time", d["standard_link"])
36
+
37
+ def test_stamp(self):
38
+ text = vst.stamp("started", vst.from_unix_ms(vst.EPOCH_UNIX_MS))
39
+ self.assertIn("started", text)
40
+ self.assertIn("VST D+000000", text)
41
+
42
+ def test_plan(self):
43
+ p = vst.Plan()
44
+ p.block("Build", flows=18000).block("Break", minutes=5)
45
+ self.assertEqual(p.total_flows, 18900)
46
+ self.assertIn("Build", p.summary())
47
+
48
+ def test_json(self):
49
+ out = vst.to_json(vst.from_unix_ms(vst.EPOCH_UNIX_MS))
50
+ self.assertEqual(json.loads(out)["flow"], 0)
51
+
52
+
53
+ if __name__ == "__main__":
54
+ unittest.main()