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.
- vishnu_standard_time-0.2.0/PKG-INFO +76 -0
- vishnu_standard_time-0.2.0/README.md +48 -0
- vishnu_standard_time-0.2.0/pyproject.toml +49 -0
- vishnu_standard_time-0.2.0/setup.cfg +4 -0
- vishnu_standard_time-0.2.0/src/vishnu_standard_time/__init__.py +371 -0
- vishnu_standard_time-0.2.0/src/vishnu_standard_time/__main__.py +3 -0
- vishnu_standard_time-0.2.0/src/vishnu_standard_time/cli.py +186 -0
- vishnu_standard_time-0.2.0/src/vishnu_standard_time.egg-info/PKG-INFO +76 -0
- vishnu_standard_time-0.2.0/src/vishnu_standard_time.egg-info/SOURCES.txt +12 -0
- vishnu_standard_time-0.2.0/src/vishnu_standard_time.egg-info/dependency_links.txt +1 -0
- vishnu_standard_time-0.2.0/src/vishnu_standard_time.egg-info/entry_points.txt +3 -0
- vishnu_standard_time-0.2.0/src/vishnu_standard_time.egg-info/top_level.txt +1 -0
- vishnu_standard_time-0.2.0/tests/test_cli_smoke.py +59 -0
- vishnu_standard_time-0.2.0/tests/test_vishnu_standard_time.py +54 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vishnu_standard_time
|
|
@@ -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()
|