fly-hour-timer 1.1.3__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,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: fly_hour_timer
3
+ Version: 1.1.3
4
+ Summary: A timezone-free Python time library: represents time as <hour>.<min_sec>
5
+ Author: fly
6
+ License: MIT
7
+ Keywords: time,timezone-free,hour-timer,ai-bot,unix-time
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: Topic :: Utilities
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+
18
+ # fly_hour_timer
19
+
20
+ **English** | [简体中文](README_zh.md)
21
+
22
+ A timezone-free Python time library that represents time as **`<hour>.<min_sec>`**.
23
+
24
+ The core idea: express time as `<hour>.<min_sec>`, where the **hour is simply
25
+ `unix_time // 3600`** (whole hours elapsed since the unix epoch) and `<min_sec>`
26
+ is the minutes and seconds inside that hour. Because everything is derived
27
+ directly from the absolute unix time, the string is fully timezone-free and
28
+ identical on any machine anywhere — especially friendly for AI bots that
29
+ exchange timestamps across regions.
30
+
31
+ - No third-party dependency, pure standard library (`datetime` / `time` only)
32
+ - Fully timezone-free by construction
33
+ - Python >= 3.8
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install fly_hour_timer
39
+ ```
40
+
41
+ Or from source:
42
+
43
+ ```bash
44
+ git clone https://github.com/fly-ttrr/fly_hour_timer.git
45
+ cd fly_hour_timer
46
+ pip install .
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ `fly_hour_timer` is a static tool — no instance configuration required:
52
+
53
+ ```python
54
+ from fly_hour_timer import fly_hour_timer
55
+
56
+ fly_hour_timer.hour() # '277777' -> hour (unix_time // 3600), str
57
+ fly_hour_timer.min_sec() # '4640' -> minutes + seconds, str
58
+ fly_hour_timer.hour_timer() # '277777.4640' -> <hour>.<min_sec>
59
+ fly_hour_timer.hour_timer_short() # '777.4640' -> <last 3 digits of hour>.<min_sec>
60
+ ```
61
+
62
+ With an explicit unix time (seconds):
63
+
64
+ ```python
65
+ from fly_hour_timer import FlyHourTimer
66
+
67
+ FlyHourTimer.hour_timer(1_000_000_000) # '277777.4640'
68
+ FlyHourTimer.hour_timer_short(1_000_000_000) # '777.4640'
69
+ ```
70
+
71
+ ## API
72
+
73
+ | Method | Description | Returns |
74
+ | --- | --- | --- |
75
+ | `hour(unix_time=None)` | Whole hours since the epoch, `unix_time // 3600` | `str` |
76
+ | `min_sec(unix_time=None)` | Minutes + seconds inside the hour, 4 digits (`'0102'` = 01m02s) | `str` |
77
+ | `hour_timer(unix_time=None)` | `<hour>.<min_sec>` | `str` |
78
+ | `hour_timer_short(unix_time=None)` | `<last 3 digits of hour>.<min_sec>` | `str` |
79
+ | `hour_timer_to_time(hour_timer)` | Parse `<hour>.<min_sec>` back into a UTC `time.struct_time` | `time.struct_time` |
80
+
81
+ ### Parameters
82
+
83
+ - **`unix_time`** — unix time in seconds (`int` / `float` / numeric `str`).
84
+ Omit to use the current time.
85
+
86
+ ## How it works
87
+
88
+ Given a unix time in seconds:
89
+
90
+ ```
91
+ hour = unix_time // 3600 # whole hours since the epoch
92
+ rem = unix_time - hour * 3600 # seconds left inside the hour (0..3599)
93
+ min_sec = "%02d%02d" % (rem // 60, rem % 60)
94
+ ```
95
+
96
+ - `hour_timer()` -> `f"{hour}.{min_sec}"`
97
+ - `hour_timer_short()` -> `f"{hour % 1000:03d}.{min_sec}"` — the last 3 digits
98
+ of the hour roll over roughly every 1000 hours (~41.7 days), giving a compact,
99
+ timezone-free time id for logs and bot conversations.
100
+
101
+ No timezone parameter is needed: the hour count and the leftover minutes/seconds
102
+ come straight from the absolute unix time, so the result is the same everywhere.
103
+
104
+ ## Reverse Conversion: `hour_timer_to_time()`
105
+
106
+ Since `<hour>` already encodes the total elapsed hours since the epoch (and thus
107
+ the full date), the conversion back is complete and needs no "today" anchor. It
108
+ returns a UTC `time.struct_time`:
109
+
110
+ ```python
111
+ from fly_hour_timer import FlyHourTimer
112
+
113
+ st = FlyHourTimer.hour_timer_to_time("277777.4640")
114
+ # time.struct_time(... 2001, 9, 9, 1, 46, 40 ...)
115
+ st.tm_hour, st.tm_min, st.tm_sec # (1, 46, 40)
116
+ ```
117
+
118
+ It is the exact inverse of `hour_timer()` for the clock fields, and raises
119
+ `ValueError` on malformed or out-of-range input (e.g. minutes `>= 60`).
120
+
121
+ > Note: pass the **full** `hour_timer()` string here. A short string like
122
+ > `'777.4640'` (from `hour_timer_short()`) has lost the high-order hour digits,
123
+ > so it reconstructs a different (much earlier) instant.
124
+
125
+ ## Examples
126
+
127
+ ```python
128
+ >>> from fly_hour_timer import FlyHourTimer
129
+ >>> FlyHourTimer.hour_timer(0)
130
+ '0.0000'
131
+ >>> FlyHourTimer.hour_timer(1_000_000_000)
132
+ '277777.4640'
133
+ >>> FlyHourTimer.hour_timer_short(1_000_000_000)
134
+ '777.4640'
135
+ ```
136
+
137
+ Run `python examples.py` in this repository for a live demo.
138
+
139
+ ## Tests
140
+
141
+ ```bash
142
+ python tests/test_fly_hour_timer.py
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,130 @@
1
+ # fly_hour_timer
2
+
3
+ **English** | [简体中文](README_zh.md)
4
+
5
+ A timezone-free Python time library that represents time as **`<hour>.<min_sec>`**.
6
+
7
+ The core idea: express time as `<hour>.<min_sec>`, where the **hour is simply
8
+ `unix_time // 3600`** (whole hours elapsed since the unix epoch) and `<min_sec>`
9
+ is the minutes and seconds inside that hour. Because everything is derived
10
+ directly from the absolute unix time, the string is fully timezone-free and
11
+ identical on any machine anywhere — especially friendly for AI bots that
12
+ exchange timestamps across regions.
13
+
14
+ - No third-party dependency, pure standard library (`datetime` / `time` only)
15
+ - Fully timezone-free by construction
16
+ - Python >= 3.8
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install fly_hour_timer
22
+ ```
23
+
24
+ Or from source:
25
+
26
+ ```bash
27
+ git clone https://github.com/fly-ttrr/fly_hour_timer.git
28
+ cd fly_hour_timer
29
+ pip install .
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ `fly_hour_timer` is a static tool — no instance configuration required:
35
+
36
+ ```python
37
+ from fly_hour_timer import fly_hour_timer
38
+
39
+ fly_hour_timer.hour() # '277777' -> hour (unix_time // 3600), str
40
+ fly_hour_timer.min_sec() # '4640' -> minutes + seconds, str
41
+ fly_hour_timer.hour_timer() # '277777.4640' -> <hour>.<min_sec>
42
+ fly_hour_timer.hour_timer_short() # '777.4640' -> <last 3 digits of hour>.<min_sec>
43
+ ```
44
+
45
+ With an explicit unix time (seconds):
46
+
47
+ ```python
48
+ from fly_hour_timer import FlyHourTimer
49
+
50
+ FlyHourTimer.hour_timer(1_000_000_000) # '277777.4640'
51
+ FlyHourTimer.hour_timer_short(1_000_000_000) # '777.4640'
52
+ ```
53
+
54
+ ## API
55
+
56
+ | Method | Description | Returns |
57
+ | --- | --- | --- |
58
+ | `hour(unix_time=None)` | Whole hours since the epoch, `unix_time // 3600` | `str` |
59
+ | `min_sec(unix_time=None)` | Minutes + seconds inside the hour, 4 digits (`'0102'` = 01m02s) | `str` |
60
+ | `hour_timer(unix_time=None)` | `<hour>.<min_sec>` | `str` |
61
+ | `hour_timer_short(unix_time=None)` | `<last 3 digits of hour>.<min_sec>` | `str` |
62
+ | `hour_timer_to_time(hour_timer)` | Parse `<hour>.<min_sec>` back into a UTC `time.struct_time` | `time.struct_time` |
63
+
64
+ ### Parameters
65
+
66
+ - **`unix_time`** — unix time in seconds (`int` / `float` / numeric `str`).
67
+ Omit to use the current time.
68
+
69
+ ## How it works
70
+
71
+ Given a unix time in seconds:
72
+
73
+ ```
74
+ hour = unix_time // 3600 # whole hours since the epoch
75
+ rem = unix_time - hour * 3600 # seconds left inside the hour (0..3599)
76
+ min_sec = "%02d%02d" % (rem // 60, rem % 60)
77
+ ```
78
+
79
+ - `hour_timer()` -> `f"{hour}.{min_sec}"`
80
+ - `hour_timer_short()` -> `f"{hour % 1000:03d}.{min_sec}"` — the last 3 digits
81
+ of the hour roll over roughly every 1000 hours (~41.7 days), giving a compact,
82
+ timezone-free time id for logs and bot conversations.
83
+
84
+ No timezone parameter is needed: the hour count and the leftover minutes/seconds
85
+ come straight from the absolute unix time, so the result is the same everywhere.
86
+
87
+ ## Reverse Conversion: `hour_timer_to_time()`
88
+
89
+ Since `<hour>` already encodes the total elapsed hours since the epoch (and thus
90
+ the full date), the conversion back is complete and needs no "today" anchor. It
91
+ returns a UTC `time.struct_time`:
92
+
93
+ ```python
94
+ from fly_hour_timer import FlyHourTimer
95
+
96
+ st = FlyHourTimer.hour_timer_to_time("277777.4640")
97
+ # time.struct_time(... 2001, 9, 9, 1, 46, 40 ...)
98
+ st.tm_hour, st.tm_min, st.tm_sec # (1, 46, 40)
99
+ ```
100
+
101
+ It is the exact inverse of `hour_timer()` for the clock fields, and raises
102
+ `ValueError` on malformed or out-of-range input (e.g. minutes `>= 60`).
103
+
104
+ > Note: pass the **full** `hour_timer()` string here. A short string like
105
+ > `'777.4640'` (from `hour_timer_short()`) has lost the high-order hour digits,
106
+ > so it reconstructs a different (much earlier) instant.
107
+
108
+ ## Examples
109
+
110
+ ```python
111
+ >>> from fly_hour_timer import FlyHourTimer
112
+ >>> FlyHourTimer.hour_timer(0)
113
+ '0.0000'
114
+ >>> FlyHourTimer.hour_timer(1_000_000_000)
115
+ '277777.4640'
116
+ >>> FlyHourTimer.hour_timer_short(1_000_000_000)
117
+ '777.4640'
118
+ ```
119
+
120
+ Run `python examples.py` in this repository for a live demo.
121
+
122
+ ## Tests
123
+
124
+ ```bash
125
+ python tests/test_fly_hour_timer.py
126
+ ```
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,127 @@
1
+ # fly_hour_timer
2
+
3
+ [English](README.md) | **简体中文**
4
+
5
+ 一个「无时区干扰」的 Python 时间库,用 **`<小时>.<分秒>`** 表示时间。
6
+
7
+ 核心思想:用 `<小时>.<分秒>` 表达时间,其中**小时就是 `unix_time // 3600`**
8
+ (自 unix 纪元以来经过的整小时数),`<分秒>` 是该小时内的分钟和秒。因为一切都
9
+ 直接由绝对的 unix 时间推导而来,所以这个字符串完全与时区无关,在任何机器、任何
10
+ 地域得到的结果都一致——尤其适合在不同地域之间交换时间戳的 AI Bot 使用。
11
+
12
+ - 无第三方依赖,纯标准库(仅用到 `datetime` / `time`)
13
+ - 与生俱来的时区无关
14
+ - 支持 Python >= 3.8
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ pip install fly_hour_timer
20
+ ```
21
+
22
+ 或从源码安装:
23
+
24
+ ```bash
25
+ git clone https://github.com/fly-ttrr/fly_hour_timer.git
26
+ cd fly_hour_timer
27
+ pip install .
28
+ ```
29
+
30
+ ## 快速开始
31
+
32
+ `fly_hour_timer` 是一个静态工具,无需创建或配置实例:
33
+
34
+ ```python
35
+ from fly_hour_timer import fly_hour_timer
36
+
37
+ fly_hour_timer.hour() # '277777' -> 小时(unix_time // 3600),str
38
+ fly_hour_timer.min_sec() # '4640' -> 分秒,str
39
+ fly_hour_timer.hour_timer() # '277777.4640' -> <小时>.<分秒>
40
+ fly_hour_timer.hour_timer_short() # '777.4640' -> <小时后三位>.<分秒>
41
+ ```
42
+
43
+ 传入指定的 unix 时间(秒):
44
+
45
+ ```python
46
+ from fly_hour_timer import FlyHourTimer
47
+
48
+ FlyHourTimer.hour_timer(1_000_000_000) # '277777.4640'
49
+ FlyHourTimer.hour_timer_short(1_000_000_000) # '777.4640'
50
+ ```
51
+
52
+ ## API
53
+
54
+ | 方法 | 说明 | 返回类型 |
55
+ | --- | --- | --- |
56
+ | `hour(unix_time=None)` | 自纪元以来的整小时数,即 `unix_time // 3600` | `str` |
57
+ | `min_sec(unix_time=None)` | 该小时内的分 + 秒拼接的 4 位(`'0102'` 表示 01 分 02 秒) | `str` |
58
+ | `hour_timer(unix_time=None)` | `<小时>.<分秒>` | `str` |
59
+ | `hour_timer_short(unix_time=None)` | `<小时后三位>.<分秒>` | `str` |
60
+ | `hour_timer_to_time(hour_timer)` | 将 `<小时>.<分秒>` 解析回 UTC 的 `time.struct_time` | `time.struct_time` |
61
+
62
+ ### 参数说明
63
+
64
+ - **`unix_time`** —— unix 时间,单位为秒(`int` / `float` / 数字字符串)。
65
+ 不传则使用当前时间。
66
+
67
+ ## 原理
68
+
69
+ 给定以秒为单位的 unix 时间:
70
+
71
+ ```
72
+ 小时 = unix_time // 3600 # 自纪元以来的整小时数
73
+ 余秒 = unix_time - 小时 * 3600 # 该小时内剩余的秒数(0..3599)
74
+ 分秒 = "%02d%02d" % (余秒 // 60, 余秒 % 60)
75
+ ```
76
+
77
+ - `hour_timer()` -> `f"{小时}.{分秒}"`
78
+ - `hour_timer_short()` -> `f"{小时 % 1000:03d}.{分秒}"`——小时后三位大约每
79
+ 1000 小时(约 41.7 天)滚动一轮,是一个紧凑、与时区无关的时间标识,适合用于
80
+ 日志与 Bot 对话。
81
+
82
+ 不需要任何时区参数:小时数与该小时内的分秒都直接来自绝对的 unix 时间,因此
83
+ 无论在哪台机器、哪个时区,结果都完全一致。
84
+
85
+ ## 逆向转换:`hour_timer_to_time()`
86
+
87
+ 由于 `<小时>` 已经编码了自纪元以来的累计小时数(因而也包含了完整日期),逆向
88
+ 转换是完备的,不需要「今天」作为锚点。它返回一个 UTC 的 `time.struct_time`:
89
+
90
+ ```python
91
+ from fly_hour_timer import FlyHourTimer
92
+
93
+ st = FlyHourTimer.hour_timer_to_time("277777.4640")
94
+ # time.struct_time(... 2001, 9, 9, 1, 46, 40 ...)
95
+ st.tm_hour, st.tm_min, st.tm_sec # (1, 46, 40)
96
+ ```
97
+
98
+ 它对时钟字段而言是 `hour_timer()` 的精确逆运算;当输入格式非法或越界(例如
99
+ 分钟 `>= 60`)时会抛出 `ValueError`。
100
+
101
+ > 注意:请传入**完整的** `hour_timer()` 字符串。像 `'777.4640'`(来自
102
+ > `hour_timer_short()`)这样的短字符串已经丢弃了高位小时数字,因此会重构成
103
+ > 一个不同的(更早的)时刻。
104
+
105
+ ## 示例
106
+
107
+ ```python
108
+ >>> from fly_hour_timer import FlyHourTimer
109
+ >>> FlyHourTimer.hour_timer(0)
110
+ '0.0000'
111
+ >>> FlyHourTimer.hour_timer(1_000_000_000)
112
+ '277777.4640'
113
+ >>> FlyHourTimer.hour_timer_short(1_000_000_000)
114
+ '777.4640'
115
+ ```
116
+
117
+ 在本仓库运行 `python examples.py` 可查看实时演示。
118
+
119
+ ## 测试
120
+
121
+ ```bash
122
+ python tests/test_fly_hour_timer.py
123
+ ```
124
+
125
+ ## 许可证
126
+
127
+ MIT
@@ -0,0 +1,10 @@
1
+ """fly_hour_timer - a timezone-free time library: <hour>.<min_sec>"""
2
+
3
+ from .fly_hour_timer import FlyHourTimer
4
+
5
+ #: The static tool (alias of the :class:`FlyHourTimer` class). Call the methods
6
+ #: directly, e.g. ``fly_hour_timer.hour_timer()``.
7
+ fly_hour_timer = FlyHourTimer
8
+
9
+ __all__ = ["FlyHourTimer", "fly_hour_timer"]
10
+ __version__ = "0.3.0"
@@ -0,0 +1,157 @@
1
+ """Core static utility of the fly_hour_timer library.
2
+
3
+ Time representation: <hour>.<min_sec>
4
+
5
+ The "hour" is simply ``unix_time // 3600`` -- the number of whole hours elapsed
6
+ since the unix epoch. ``<min_sec>`` is the minutes and seconds inside that
7
+ hour. Because everything is derived directly from the absolute unix time, the
8
+ string is fully timezone-free and identical on any machine anywhere.
9
+ """
10
+
11
+ import time as _time
12
+ from datetime import datetime, timezone as dt_timezone
13
+
14
+ __all__ = ["FlyHourTimer"]
15
+
16
+
17
+ class FlyHourTimer:
18
+ """Static time tool: represents time as ``<hour>.<min_sec>``.
19
+
20
+ All methods are static; no instance is required.
21
+
22
+ >>> FlyHourTimer.hour_timer(1_000_000_000) # 2001-09-09 01:46:40 UTC
23
+ '277777.4640'
24
+ >>> FlyHourTimer.hour_timer_short(1_000_000_000)
25
+ '777.4640'
26
+ >>> FlyHourTimer.hour_timer(0) # the epoch
27
+ '0.0000'
28
+ """
29
+
30
+ # ------------------------------------------------------------------
31
+ # public API
32
+ # ------------------------------------------------------------------
33
+
34
+ @staticmethod
35
+ def hour(unix_time=None):
36
+ """Return the hour: whole hours elapsed since the unix epoch.
37
+
38
+ Equivalent to ``unix_time // 3600``.
39
+
40
+ Args:
41
+ unix_time: unix time in seconds (int/float/numeric str).
42
+ Defaults to the current time when omitted.
43
+
44
+ Returns:
45
+ str: e.g. ``'277777'``.
46
+ """
47
+ hours, _, _ = FlyHourTimer._split(unix_time)
48
+ return str(hours)
49
+
50
+ @staticmethod
51
+ def min_sec(unix_time=None):
52
+ """Return the ``<min_sec>`` part: minutes and seconds inside the hour.
53
+
54
+ Args:
55
+ unix_time: unix time in seconds, defaults to the current time.
56
+
57
+ Returns:
58
+ str: 4 digits ``MMSS``, e.g. ``'0102'`` means 01 min 02 sec.
59
+ """
60
+ _, minute, second = FlyHourTimer._split(unix_time)
61
+ return f"{minute:02d}{second:02d}"
62
+
63
+ @staticmethod
64
+ def hour_timer(unix_time=None):
65
+ """Return time as ``<hour>.<min_sec>``, where hour = ``unix_time // 3600``.
66
+
67
+ Args:
68
+ unix_time: unix time in seconds, defaults to the current time.
69
+
70
+ Returns:
71
+ str: e.g. ``'277777.4640'``.
72
+ """
73
+ hours, minute, second = FlyHourTimer._split(unix_time)
74
+ return f"{hours}.{minute:02d}{second:02d}"
75
+
76
+ @staticmethod
77
+ def hour_timer_short(unix_time=None):
78
+ """Return time as ``<last 3 digits of hour>.<min_sec>``.
79
+
80
+ The last 3 digits of the hour roll over roughly every 1000 hours
81
+ (~41.7 days), giving a compact, timezone-free time id for logs and
82
+ bot conversations.
83
+
84
+ Args:
85
+ unix_time: unix time in seconds, defaults to the current time.
86
+
87
+ Returns:
88
+ str: e.g. hour ``277777`` -> ``'777.4640'``.
89
+ """
90
+ hours, minute, second = FlyHourTimer._split(unix_time)
91
+ return f"{hours % 1000:03d}.{minute:02d}{second:02d}"
92
+
93
+ @staticmethod
94
+ def hour_timer_to_time(hour_timer):
95
+ """Convert a ``<hour>.<min_sec>`` string into a ``time.struct_time``.
96
+
97
+ Because ``<hour>`` already encodes the total elapsed hours since the
98
+ epoch (and therefore the date), this is a complete, timezone-free
99
+ inverse of :meth:`hour_timer` -- no "today" anchor is needed. The
100
+ result is UTC based (``time.gmtime``).
101
+
102
+ Args:
103
+ hour_timer: the ``<hour>.<min_sec>`` string, e.g.
104
+ ``'277777.4640'``.
105
+
106
+ Returns:
107
+ time.struct_time: UTC struct_time reconstructed from the string.
108
+
109
+ Raises:
110
+ ValueError: if ``hour_timer`` is malformed or out of range.
111
+ """
112
+ hours, minute, second = FlyHourTimer._parse_hour_timer(hour_timer)
113
+ unix_time = hours * 3600 + minute * 60 + second
114
+ return _time.gmtime(unix_time)
115
+
116
+ # ------------------------------------------------------------------
117
+ # internals
118
+ # ------------------------------------------------------------------
119
+
120
+ @staticmethod
121
+ def _split(unix_time):
122
+ """Split a unix time into (total_hours, minute, second)."""
123
+ if unix_time is None:
124
+ ts = datetime.now(dt_timezone.utc).timestamp()
125
+ else:
126
+ ts = float(unix_time)
127
+ hours = int(ts // 3600)
128
+ rem = ts - hours * 3600 # seconds inside the hour, always in [0, 3600)
129
+ minute = int(rem // 60)
130
+ second = int(rem % 60)
131
+ return hours, minute, second
132
+
133
+ @staticmethod
134
+ def _parse_hour_timer(hour_timer):
135
+ """Parse ``'<hour>.<min_sec>'`` into (total_hours, minute, second)."""
136
+ if not isinstance(hour_timer, str):
137
+ raise ValueError(f"hour_timer must be a str, got {hour_timer!r}")
138
+ text = hour_timer.strip()
139
+ if text.count(".") != 1:
140
+ raise ValueError(
141
+ f"hour_timer must look like '<hour>.<min_sec>', got {hour_timer!r}"
142
+ )
143
+ hour_part, min_sec_part = text.split(".", 1)
144
+ if not min_sec_part.isdigit() or len(min_sec_part) != 4:
145
+ raise ValueError(
146
+ f"hour_timer '<min_sec>' must be 4 digits, got {hour_timer!r}"
147
+ )
148
+ try:
149
+ hours = int(hour_part)
150
+ except ValueError:
151
+ raise ValueError(
152
+ f"hour_timer '<hour>' must be an integer, got {hour_timer!r}"
153
+ ) from None
154
+ minute, second = int(min_sec_part[:2]), int(min_sec_part[2:])
155
+ if not (0 <= minute <= 59 and 0 <= second <= 59):
156
+ raise ValueError(f"hour_timer '<min_sec>' out of range, got {hour_timer!r}")
157
+ return hours, minute, second
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: fly_hour_timer
3
+ Version: 1.1.3
4
+ Summary: A timezone-free Python time library: represents time as <hour>.<min_sec>
5
+ Author: fly
6
+ License: MIT
7
+ Keywords: time,timezone-free,hour-timer,ai-bot,unix-time
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: Topic :: Utilities
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+
18
+ # fly_hour_timer
19
+
20
+ **English** | [简体中文](README_zh.md)
21
+
22
+ A timezone-free Python time library that represents time as **`<hour>.<min_sec>`**.
23
+
24
+ The core idea: express time as `<hour>.<min_sec>`, where the **hour is simply
25
+ `unix_time // 3600`** (whole hours elapsed since the unix epoch) and `<min_sec>`
26
+ is the minutes and seconds inside that hour. Because everything is derived
27
+ directly from the absolute unix time, the string is fully timezone-free and
28
+ identical on any machine anywhere — especially friendly for AI bots that
29
+ exchange timestamps across regions.
30
+
31
+ - No third-party dependency, pure standard library (`datetime` / `time` only)
32
+ - Fully timezone-free by construction
33
+ - Python >= 3.8
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install fly_hour_timer
39
+ ```
40
+
41
+ Or from source:
42
+
43
+ ```bash
44
+ git clone https://github.com/fly-ttrr/fly_hour_timer.git
45
+ cd fly_hour_timer
46
+ pip install .
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ `fly_hour_timer` is a static tool — no instance configuration required:
52
+
53
+ ```python
54
+ from fly_hour_timer import fly_hour_timer
55
+
56
+ fly_hour_timer.hour() # '277777' -> hour (unix_time // 3600), str
57
+ fly_hour_timer.min_sec() # '4640' -> minutes + seconds, str
58
+ fly_hour_timer.hour_timer() # '277777.4640' -> <hour>.<min_sec>
59
+ fly_hour_timer.hour_timer_short() # '777.4640' -> <last 3 digits of hour>.<min_sec>
60
+ ```
61
+
62
+ With an explicit unix time (seconds):
63
+
64
+ ```python
65
+ from fly_hour_timer import FlyHourTimer
66
+
67
+ FlyHourTimer.hour_timer(1_000_000_000) # '277777.4640'
68
+ FlyHourTimer.hour_timer_short(1_000_000_000) # '777.4640'
69
+ ```
70
+
71
+ ## API
72
+
73
+ | Method | Description | Returns |
74
+ | --- | --- | --- |
75
+ | `hour(unix_time=None)` | Whole hours since the epoch, `unix_time // 3600` | `str` |
76
+ | `min_sec(unix_time=None)` | Minutes + seconds inside the hour, 4 digits (`'0102'` = 01m02s) | `str` |
77
+ | `hour_timer(unix_time=None)` | `<hour>.<min_sec>` | `str` |
78
+ | `hour_timer_short(unix_time=None)` | `<last 3 digits of hour>.<min_sec>` | `str` |
79
+ | `hour_timer_to_time(hour_timer)` | Parse `<hour>.<min_sec>` back into a UTC `time.struct_time` | `time.struct_time` |
80
+
81
+ ### Parameters
82
+
83
+ - **`unix_time`** — unix time in seconds (`int` / `float` / numeric `str`).
84
+ Omit to use the current time.
85
+
86
+ ## How it works
87
+
88
+ Given a unix time in seconds:
89
+
90
+ ```
91
+ hour = unix_time // 3600 # whole hours since the epoch
92
+ rem = unix_time - hour * 3600 # seconds left inside the hour (0..3599)
93
+ min_sec = "%02d%02d" % (rem // 60, rem % 60)
94
+ ```
95
+
96
+ - `hour_timer()` -> `f"{hour}.{min_sec}"`
97
+ - `hour_timer_short()` -> `f"{hour % 1000:03d}.{min_sec}"` — the last 3 digits
98
+ of the hour roll over roughly every 1000 hours (~41.7 days), giving a compact,
99
+ timezone-free time id for logs and bot conversations.
100
+
101
+ No timezone parameter is needed: the hour count and the leftover minutes/seconds
102
+ come straight from the absolute unix time, so the result is the same everywhere.
103
+
104
+ ## Reverse Conversion: `hour_timer_to_time()`
105
+
106
+ Since `<hour>` already encodes the total elapsed hours since the epoch (and thus
107
+ the full date), the conversion back is complete and needs no "today" anchor. It
108
+ returns a UTC `time.struct_time`:
109
+
110
+ ```python
111
+ from fly_hour_timer import FlyHourTimer
112
+
113
+ st = FlyHourTimer.hour_timer_to_time("277777.4640")
114
+ # time.struct_time(... 2001, 9, 9, 1, 46, 40 ...)
115
+ st.tm_hour, st.tm_min, st.tm_sec # (1, 46, 40)
116
+ ```
117
+
118
+ It is the exact inverse of `hour_timer()` for the clock fields, and raises
119
+ `ValueError` on malformed or out-of-range input (e.g. minutes `>= 60`).
120
+
121
+ > Note: pass the **full** `hour_timer()` string here. A short string like
122
+ > `'777.4640'` (from `hour_timer_short()`) has lost the high-order hour digits,
123
+ > so it reconstructs a different (much earlier) instant.
124
+
125
+ ## Examples
126
+
127
+ ```python
128
+ >>> from fly_hour_timer import FlyHourTimer
129
+ >>> FlyHourTimer.hour_timer(0)
130
+ '0.0000'
131
+ >>> FlyHourTimer.hour_timer(1_000_000_000)
132
+ '277777.4640'
133
+ >>> FlyHourTimer.hour_timer_short(1_000_000_000)
134
+ '777.4640'
135
+ ```
136
+
137
+ Run `python examples.py` in this repository for a live demo.
138
+
139
+ ## Tests
140
+
141
+ ```bash
142
+ python tests/test_fly_hour_timer.py
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,10 @@
1
+ README.md
2
+ README_zh.md
3
+ pyproject.toml
4
+ fly_hour_timer/__init__.py
5
+ fly_hour_timer/fly_hour_timer.py
6
+ fly_hour_timer.egg-info/PKG-INFO
7
+ fly_hour_timer.egg-info/SOURCES.txt
8
+ fly_hour_timer.egg-info/dependency_links.txt
9
+ fly_hour_timer.egg-info/top_level.txt
10
+ tests/test_fly_hour_timer.py
@@ -0,0 +1 @@
1
+ fly_hour_timer
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "fly_hour_timer"
7
+ version = "1.1.3"
8
+ description = "A timezone-free Python time library: represents time as <hour>.<min_sec>"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ keywords = ["time", "timezone-free", "hour-timer", "ai-bot", "unix-time"]
13
+ authors = [{ name = "fly" }]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ "Topic :: Utilities",
22
+ ]
23
+
24
+ [tool.pytest.ini_options]
25
+ addopts = "-p no:dash"
26
+
27
+ [tool.setuptools]
28
+ packages = ["fly_hour_timer"]
29
+
30
+ [tool.setuptools.data-files]
31
+ "share/fly_hour_timer" = ["README_zh.md"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,124 @@
1
+ """Unit tests for fly_hour_timer (no third-party dependency needed).
2
+
3
+ Run: python tests/test_fly_hour_timer.py
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import time
9
+ import unittest
10
+ from datetime import datetime, timezone as dt_timezone
11
+
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+
14
+ from fly_hour_timer import FlyHourTimer, fly_hour_timer
15
+
16
+ # 2001-09-09 01:46:40 UTC -> 1_000_000_000 // 3600 == 277777 hours
17
+ UNIX_911 = 1_000_000_000
18
+
19
+
20
+ class TestStaticMethods(unittest.TestCase):
21
+ def test_hour(self):
22
+ self.assertEqual(FlyHourTimer.hour(UNIX_911), "277777")
23
+
24
+ def test_hour_epoch(self):
25
+ self.assertEqual(FlyHourTimer.hour(0), "0")
26
+
27
+ def test_min_sec(self):
28
+ self.assertEqual(FlyHourTimer.min_sec(UNIX_911), "4640")
29
+
30
+ def test_min_sec_padding(self):
31
+ # 1970-01-01 00:01:02 UTC -> 01 min 02 sec
32
+ self.assertEqual(FlyHourTimer.min_sec(62), "0102")
33
+
34
+ def test_hour_timer(self):
35
+ self.assertEqual(FlyHourTimer.hour_timer(UNIX_911), "277777.4640")
36
+
37
+ def test_hour_timer_epoch(self):
38
+ self.assertEqual(FlyHourTimer.hour_timer(0), "0.0000")
39
+
40
+ def test_hour_timer_short(self):
41
+ self.assertEqual(FlyHourTimer.hour_timer_short(UNIX_911), "777.4640")
42
+
43
+ def test_hour_timer_short_epoch(self):
44
+ self.assertEqual(FlyHourTimer.hour_timer_short(0), "000.0000")
45
+
46
+ def test_hour_matches_division(self):
47
+ # the hour is exactly unix_time // 3600, timezone-free
48
+ for ts in (0, 3599, 3600, 7199, UNIX_911, 1_700_000_000):
49
+ self.assertEqual(
50
+ FlyHourTimer.hour(ts), str(ts // 3600)
51
+ )
52
+
53
+ def test_min_sec_matches_remainder(self):
54
+ # min_sec reflects the seconds left over after whole hours
55
+ for ts in (62, 3599, UNIX_911):
56
+ rem = ts - (ts // 3600) * 3600
57
+ expected = f"{rem // 60:02d}{rem % 60:02d}"
58
+ self.assertEqual(FlyHourTimer.min_sec(ts), expected)
59
+
60
+ def test_unix_time_float_and_str(self):
61
+ self.assertEqual(FlyHourTimer.hour_timer(1_000_000_000.0), "277777.4640")
62
+ self.assertEqual(FlyHourTimer.hour_timer("1000000000"), "277777.4640")
63
+
64
+ def test_default_returns_str(self):
65
+ for value in (
66
+ FlyHourTimer.hour(),
67
+ FlyHourTimer.min_sec(),
68
+ FlyHourTimer.hour_timer(),
69
+ FlyHourTimer.hour_timer_short(),
70
+ ):
71
+ self.assertIsInstance(value, str)
72
+
73
+ def test_output_matches_current_time(self):
74
+ expected = FlyHourTimer.hour_timer(
75
+ int(datetime.now(dt_timezone.utc).timestamp())
76
+ )
77
+ actual = FlyHourTimer.hour_timer()
78
+ # allow the whole-second boundary to differ between the two reads
79
+ self.assertEqual(expected.split(".")[0], actual.split(".")[0])
80
+
81
+
82
+ class TestHourTimerToTime(unittest.TestCase):
83
+ def test_returns_struct_time(self):
84
+ self.assertIsInstance(
85
+ FlyHourTimer.hour_timer_to_time("277777.4640"), time.struct_time
86
+ )
87
+
88
+ def test_full_round_trip(self):
89
+ # hour_timer -> hour_timer_to_time reconstructs the exact UTC struct
90
+ st = FlyHourTimer.hour_timer_to_time(FlyHourTimer.hour_timer(UNIX_911))
91
+ self.assertEqual(st, time.gmtime(UNIX_911))
92
+
93
+ def test_fields(self):
94
+ st = FlyHourTimer.hour_timer_to_time("277777.4640")
95
+ self.assertEqual(
96
+ (st.tm_year, st.tm_mon, st.tm_mday, st.tm_hour, st.tm_min, st.tm_sec),
97
+ (2001, 9, 9, 1, 46, 40),
98
+ )
99
+
100
+ def test_epoch(self):
101
+ self.assertEqual(FlyHourTimer.hour_timer_to_time("0.0000"), time.gmtime(0))
102
+
103
+ def test_encodes_date_not_today(self):
104
+ # the hour number carries the date, so it is NOT anchored to today
105
+ st = FlyHourTimer.hour_timer_to_time("277777.4640")
106
+ self.assertEqual(st.tm_year, 2001)
107
+
108
+ def test_invalid_format_raises(self):
109
+ for bad in ("", "12", "12.345", "12.34567", "ab.cd", None,
110
+ "1.6060", "1.-1.23", "12.34 56"):
111
+ with self.assertRaises(ValueError):
112
+ FlyHourTimer.hour_timer_to_time(bad)
113
+
114
+
115
+ class TestStaticAlias(unittest.TestCase):
116
+ def test_alias_is_class(self):
117
+ self.assertIs(fly_hour_timer, FlyHourTimer)
118
+
119
+ def test_alias_call(self):
120
+ self.assertEqual(fly_hour_timer.hour_timer(UNIX_911), "277777.4640")
121
+
122
+
123
+ if __name__ == "__main__":
124
+ unittest.main(verbosity=2)