rsTimes 0.1.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.
Potentially problematic release.
This version of rsTimes might be problematic. Click here for more details.
- rstimes-0.1.0/LICENSE +21 -0
- rstimes-0.1.0/PKG-INFO +133 -0
- rstimes-0.1.0/README.md +118 -0
- rstimes-0.1.0/pyproject.toml +37 -0
- rstimes-0.1.0/setup.cfg +4 -0
- rstimes-0.1.0/src/rsTimes.egg-info/PKG-INFO +133 -0
- rstimes-0.1.0/src/rsTimes.egg-info/SOURCES.txt +11 -0
- rstimes-0.1.0/src/rsTimes.egg-info/dependency_links.txt +1 -0
- rstimes-0.1.0/src/rsTimes.egg-info/requires.txt +3 -0
- rstimes-0.1.0/src/rsTimes.egg-info/top_level.txt +1 -0
- rstimes-0.1.0/src/rstimes/__init__.py +47 -0
- rstimes-0.1.0/src/rstimes/py.typed +0 -0
- rstimes-0.1.0/tests/test_rstimes.py +91 -0
rstimes-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jiefu Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rstimes-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rsTimes
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Integer-based conversions between aware datetimes and Unix milliseconds
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
|
+
Classifier: Typing :: Typed
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# rsTimes
|
|
17
|
+
|
|
18
|
+
`rsTimes` 0.1.0 是一个纯 Python 日期时间小工具,提供带时区 `datetime`
|
|
19
|
+
与 Unix 毫秒时间戳的双向转换。没有运行时依赖,要求 Python 3.10 或更新版本。
|
|
20
|
+
当前版本没有 Rust 内核,不承诺性能优势或与其他日期时间库兼容。
|
|
21
|
+
|
|
22
|
+
PyPI 分发名是 `rsTimes`,Python 导入名是 `rstimes`。
|
|
23
|
+
|
|
24
|
+
## 使用
|
|
25
|
+
|
|
26
|
+
首次成功发布到 PyPI 后可安装:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
python -m pip install rsTimes
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from datetime import datetime, timedelta, timezone
|
|
34
|
+
from rstimes import from_unix_ms, to_unix_ms
|
|
35
|
+
|
|
36
|
+
local_epoch = datetime(1970, 1, 1, 8, tzinfo=timezone(timedelta(hours=8)))
|
|
37
|
+
assert to_unix_ms(local_epoch) == 0
|
|
38
|
+
assert from_unix_ms(0) == datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
39
|
+
|
|
40
|
+
before_epoch = datetime(1969, 12, 31, 23, 59, 59, 999999, tzinfo=timezone.utc)
|
|
41
|
+
assert to_unix_ms(before_epoch) == -1
|
|
42
|
+
assert from_unix_ms(-1).isoformat() == "1969-12-31T23:59:59.999000+00:00"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 转换规则
|
|
46
|
+
|
|
47
|
+
- `to_unix_ms(value: datetime) -> int`:输入必须带有效 UTC 偏移,
|
|
48
|
+
转换到 UTC 后用整数计算。微秒余数**向负无穷方向取整**:
|
|
49
|
+
纪元后 999 微秒得到 `0`,纪元前 1 微秒得到 `-1`。
|
|
50
|
+
- `from_unix_ms(value: int) -> datetime`:只接受整数,拒绝 `bool`、
|
|
51
|
+
浮点数、字符串等类型;返回 `timezone.utc` 的带时区时间。
|
|
52
|
+
- 毫秒范围为 `-62135596800000` 到 `253402300799999`,含两端,
|
|
53
|
+
对应 UTC 年份 1–9999。超出范围或时区转换越界抛出 `ValueError`;
|
|
54
|
+
naive datetime 也抛出 `ValueError`,非法输入类型抛出 `TypeError`。
|
|
55
|
+
- 不使用本机时区、不经过浮点时间戳。毫秒整数可以精确往返;
|
|
56
|
+
datetime 的不足 1 毫秒部分在转换时会丢失。
|
|
57
|
+
|
|
58
|
+
## 本地开发与构建
|
|
59
|
+
|
|
60
|
+
以下命令适用于 macOS/Linux;Windows 可用 `.venv\Scripts\python`。
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
python3 -m venv .venv
|
|
64
|
+
.venv/bin/python -m pip install --index-url https://pypi.org/simple -e ".[test]" build twine
|
|
65
|
+
.venv/bin/python -m pytest
|
|
66
|
+
.venv/bin/python -m build
|
|
67
|
+
.venv/bin/python -m twine check --strict dist/*
|
|
68
|
+
|
|
69
|
+
.venv/bin/python -m venv .wheel-test
|
|
70
|
+
.wheel-test/bin/python -m pip install --no-deps dist/*.whl
|
|
71
|
+
.wheel-test/bin/python -I -c 'from rstimes import from_unix_ms, to_unix_ms; assert to_unix_ms(from_unix_ms(-1)) == -1'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
重新构建前清理旧的 `dist/` 内容,避免混入以前的发布文件。
|
|
75
|
+
CI 配置会在 GitHub 的 Ubuntu runner 上使用 Python 3.10 和 3.13 测试、构建、
|
|
76
|
+
检查元数据,并从 wheel 安装验证导入;这些配置不代表已在所有平台完成测试。
|
|
77
|
+
|
|
78
|
+
## 发布前需要填写的信息
|
|
79
|
+
|
|
80
|
+
维护者需要自行填写真实信息:
|
|
81
|
+
|
|
82
|
+
| 项目 | 当前状态 |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| GitHub owner | 待填写本人账号或组织名 |
|
|
85
|
+
| GitHub repository | 待填写实际仓库名 |
|
|
86
|
+
| 许可证 | [MIT](LICENSE),Copyright (c) 2026 Jiefu Chen |
|
|
87
|
+
| 作者与项目 URL | 尚未填写;按实际情况补充,不使用虚构身份或链接 |
|
|
88
|
+
|
|
89
|
+
本项目采用 [MIT 许可证](LICENSE),许可证文本随源码与分发包一同提供。
|
|
90
|
+
|
|
91
|
+
PyPI 将没有功能或空包的占名项目视为无效项目。本版本提供实际的时间转换功能,
|
|
92
|
+
README 和包描述应保持与实际功能一致。
|
|
93
|
+
|
|
94
|
+
## GitHub → PyPI Trusted Publisher
|
|
95
|
+
|
|
96
|
+
本地配置已经准备好,以下持久授权和发布操作由维护者完成:
|
|
97
|
+
|
|
98
|
+
1. 补齐需要的真实元数据;自行把这些文件提交并推送到自己的 GitHub
|
|
99
|
+
仓库。CI 成功后再准备正式 release。
|
|
100
|
+
2. 在该 GitHub 仓库的 Settings → Environments 中创建名为 `pypi` 的 environment。
|
|
101
|
+
如有需要,为它配置审批人和允许发布的 tag 规则。
|
|
102
|
+
3. 若 PyPI 尚无该项目,在自己的 PyPI 账号 Publishing 页面添加 **Pending Publisher**,
|
|
103
|
+
选择 GitHub,准确填写下表。已有项目则在该项目的 Publishing 页面添加 publisher。
|
|
104
|
+
|
|
105
|
+
| PyPI 字段 | 必须填写的值 |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| PyPI Project Name(pending 时) | `rsTimes` |
|
|
108
|
+
| Owner | 实际 GitHub owner,须与仓库所属账号或组织一致 |
|
|
109
|
+
| Repository name | 实际 GitHub 仓库名 |
|
|
110
|
+
| Workflow name | `publish.yml`(文件名,不是 workflow 显示名称) |
|
|
111
|
+
| Environment name | `pypi` |
|
|
112
|
+
|
|
113
|
+
Pending Publisher 在首次成功上传前不会创建项目或预留包名;名称也可能已被占用
|
|
114
|
+
或被 PyPI 保留。首次成功发布后 pending publisher 转为正式 publisher。
|
|
115
|
+
4. 当前 `pyproject.toml` 的版本为 `0.1.0`。在包含上述文件的 commit 上创建 tag
|
|
116
|
+
`v0.1.0`,再发布对应的 GitHub Release。仅推送 tag 或保存 release 草稿不会触发上传。
|
|
117
|
+
5. `.github/workflows/publish.yml` 只响应 `release: published`,检查 tag 与包版本
|
|
118
|
+
精确匹配(必须是 `v` 加版本号),随后测试、构建、检查 sdist/wheel 并验证 wheel。
|
|
119
|
+
只有这些步骤全部成功才会进入发布 job。
|
|
120
|
+
6. 发布 job 使用 `environment: pypi`,仅该 job 有 `id-token: write`。
|
|
121
|
+
它只下载本次运行的构建产物,使用 PyPA 官方 action 的 OIDC 身份发布,
|
|
122
|
+
不需要设置 `PYPI_TOKEN` 或用户名/密码 secret。
|
|
123
|
+
|
|
124
|
+
PyPI 发布是公开且持久的;首次发布前检查 wheel/sdist 内容与真实元数据。
|
|
125
|
+
新版本请先修改 `pyproject.toml`,重新验证,再创建匹配的新 tag/release。
|
|
126
|
+
已经用过的版本和分发文件不能直接覆盖。
|
|
127
|
+
|
|
128
|
+
官方说明:
|
|
129
|
+
|
|
130
|
+
- [Python 项目打包](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
|
|
131
|
+
- [创建 Pending Trusted Publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
|
|
132
|
+
- [通过 Trusted Publisher 发布](https://docs.pypi.org/trusted-publishers/using-a-publisher/)
|
|
133
|
+
- [PyPI 名称政策](https://docs.pypi.org/project-management/name-retention/#invalid-projects)
|
rstimes-0.1.0/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# rsTimes
|
|
2
|
+
|
|
3
|
+
`rsTimes` 0.1.0 是一个纯 Python 日期时间小工具,提供带时区 `datetime`
|
|
4
|
+
与 Unix 毫秒时间戳的双向转换。没有运行时依赖,要求 Python 3.10 或更新版本。
|
|
5
|
+
当前版本没有 Rust 内核,不承诺性能优势或与其他日期时间库兼容。
|
|
6
|
+
|
|
7
|
+
PyPI 分发名是 `rsTimes`,Python 导入名是 `rstimes`。
|
|
8
|
+
|
|
9
|
+
## 使用
|
|
10
|
+
|
|
11
|
+
首次成功发布到 PyPI 后可安装:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
python -m pip install rsTimes
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from datetime import datetime, timedelta, timezone
|
|
19
|
+
from rstimes import from_unix_ms, to_unix_ms
|
|
20
|
+
|
|
21
|
+
local_epoch = datetime(1970, 1, 1, 8, tzinfo=timezone(timedelta(hours=8)))
|
|
22
|
+
assert to_unix_ms(local_epoch) == 0
|
|
23
|
+
assert from_unix_ms(0) == datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
24
|
+
|
|
25
|
+
before_epoch = datetime(1969, 12, 31, 23, 59, 59, 999999, tzinfo=timezone.utc)
|
|
26
|
+
assert to_unix_ms(before_epoch) == -1
|
|
27
|
+
assert from_unix_ms(-1).isoformat() == "1969-12-31T23:59:59.999000+00:00"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 转换规则
|
|
31
|
+
|
|
32
|
+
- `to_unix_ms(value: datetime) -> int`:输入必须带有效 UTC 偏移,
|
|
33
|
+
转换到 UTC 后用整数计算。微秒余数**向负无穷方向取整**:
|
|
34
|
+
纪元后 999 微秒得到 `0`,纪元前 1 微秒得到 `-1`。
|
|
35
|
+
- `from_unix_ms(value: int) -> datetime`:只接受整数,拒绝 `bool`、
|
|
36
|
+
浮点数、字符串等类型;返回 `timezone.utc` 的带时区时间。
|
|
37
|
+
- 毫秒范围为 `-62135596800000` 到 `253402300799999`,含两端,
|
|
38
|
+
对应 UTC 年份 1–9999。超出范围或时区转换越界抛出 `ValueError`;
|
|
39
|
+
naive datetime 也抛出 `ValueError`,非法输入类型抛出 `TypeError`。
|
|
40
|
+
- 不使用本机时区、不经过浮点时间戳。毫秒整数可以精确往返;
|
|
41
|
+
datetime 的不足 1 毫秒部分在转换时会丢失。
|
|
42
|
+
|
|
43
|
+
## 本地开发与构建
|
|
44
|
+
|
|
45
|
+
以下命令适用于 macOS/Linux;Windows 可用 `.venv\Scripts\python`。
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
python3 -m venv .venv
|
|
49
|
+
.venv/bin/python -m pip install --index-url https://pypi.org/simple -e ".[test]" build twine
|
|
50
|
+
.venv/bin/python -m pytest
|
|
51
|
+
.venv/bin/python -m build
|
|
52
|
+
.venv/bin/python -m twine check --strict dist/*
|
|
53
|
+
|
|
54
|
+
.venv/bin/python -m venv .wheel-test
|
|
55
|
+
.wheel-test/bin/python -m pip install --no-deps dist/*.whl
|
|
56
|
+
.wheel-test/bin/python -I -c 'from rstimes import from_unix_ms, to_unix_ms; assert to_unix_ms(from_unix_ms(-1)) == -1'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
重新构建前清理旧的 `dist/` 内容,避免混入以前的发布文件。
|
|
60
|
+
CI 配置会在 GitHub 的 Ubuntu runner 上使用 Python 3.10 和 3.13 测试、构建、
|
|
61
|
+
检查元数据,并从 wheel 安装验证导入;这些配置不代表已在所有平台完成测试。
|
|
62
|
+
|
|
63
|
+
## 发布前需要填写的信息
|
|
64
|
+
|
|
65
|
+
维护者需要自行填写真实信息:
|
|
66
|
+
|
|
67
|
+
| 项目 | 当前状态 |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| GitHub owner | 待填写本人账号或组织名 |
|
|
70
|
+
| GitHub repository | 待填写实际仓库名 |
|
|
71
|
+
| 许可证 | [MIT](LICENSE),Copyright (c) 2026 Jiefu Chen |
|
|
72
|
+
| 作者与项目 URL | 尚未填写;按实际情况补充,不使用虚构身份或链接 |
|
|
73
|
+
|
|
74
|
+
本项目采用 [MIT 许可证](LICENSE),许可证文本随源码与分发包一同提供。
|
|
75
|
+
|
|
76
|
+
PyPI 将没有功能或空包的占名项目视为无效项目。本版本提供实际的时间转换功能,
|
|
77
|
+
README 和包描述应保持与实际功能一致。
|
|
78
|
+
|
|
79
|
+
## GitHub → PyPI Trusted Publisher
|
|
80
|
+
|
|
81
|
+
本地配置已经准备好,以下持久授权和发布操作由维护者完成:
|
|
82
|
+
|
|
83
|
+
1. 补齐需要的真实元数据;自行把这些文件提交并推送到自己的 GitHub
|
|
84
|
+
仓库。CI 成功后再准备正式 release。
|
|
85
|
+
2. 在该 GitHub 仓库的 Settings → Environments 中创建名为 `pypi` 的 environment。
|
|
86
|
+
如有需要,为它配置审批人和允许发布的 tag 规则。
|
|
87
|
+
3. 若 PyPI 尚无该项目,在自己的 PyPI 账号 Publishing 页面添加 **Pending Publisher**,
|
|
88
|
+
选择 GitHub,准确填写下表。已有项目则在该项目的 Publishing 页面添加 publisher。
|
|
89
|
+
|
|
90
|
+
| PyPI 字段 | 必须填写的值 |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| PyPI Project Name(pending 时) | `rsTimes` |
|
|
93
|
+
| Owner | 实际 GitHub owner,须与仓库所属账号或组织一致 |
|
|
94
|
+
| Repository name | 实际 GitHub 仓库名 |
|
|
95
|
+
| Workflow name | `publish.yml`(文件名,不是 workflow 显示名称) |
|
|
96
|
+
| Environment name | `pypi` |
|
|
97
|
+
|
|
98
|
+
Pending Publisher 在首次成功上传前不会创建项目或预留包名;名称也可能已被占用
|
|
99
|
+
或被 PyPI 保留。首次成功发布后 pending publisher 转为正式 publisher。
|
|
100
|
+
4. 当前 `pyproject.toml` 的版本为 `0.1.0`。在包含上述文件的 commit 上创建 tag
|
|
101
|
+
`v0.1.0`,再发布对应的 GitHub Release。仅推送 tag 或保存 release 草稿不会触发上传。
|
|
102
|
+
5. `.github/workflows/publish.yml` 只响应 `release: published`,检查 tag 与包版本
|
|
103
|
+
精确匹配(必须是 `v` 加版本号),随后测试、构建、检查 sdist/wheel 并验证 wheel。
|
|
104
|
+
只有这些步骤全部成功才会进入发布 job。
|
|
105
|
+
6. 发布 job 使用 `environment: pypi`,仅该 job 有 `id-token: write`。
|
|
106
|
+
它只下载本次运行的构建产物,使用 PyPA 官方 action 的 OIDC 身份发布,
|
|
107
|
+
不需要设置 `PYPI_TOKEN` 或用户名/密码 secret。
|
|
108
|
+
|
|
109
|
+
PyPI 发布是公开且持久的;首次发布前检查 wheel/sdist 内容与真实元数据。
|
|
110
|
+
新版本请先修改 `pyproject.toml`,重新验证,再创建匹配的新 tag/release。
|
|
111
|
+
已经用过的版本和分发文件不能直接覆盖。
|
|
112
|
+
|
|
113
|
+
官方说明:
|
|
114
|
+
|
|
115
|
+
- [Python 项目打包](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
|
|
116
|
+
- [创建 Pending Trusted Publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
|
|
117
|
+
- [通过 Trusted Publisher 发布](https://docs.pypi.org/trusted-publishers/using-a-publisher/)
|
|
118
|
+
- [PyPI 名称政策](https://docs.pypi.org/project-management/name-retention/#invalid-projects)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rsTimes"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Integer-based conversions between aware datetimes and Unix milliseconds"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = []
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
# Authors and repository URLs await the maintainer's decision.
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
test = ["pytest>=8"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
package-dir = {"" = "src"}
|
|
26
|
+
include-package-data = false
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
include = ["rstimes"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
rstimes = ["py.typed"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
addopts = "-q"
|
rstimes-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rsTimes
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Integer-based conversions between aware datetimes and Unix milliseconds
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
|
+
Classifier: Typing :: Typed
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# rsTimes
|
|
17
|
+
|
|
18
|
+
`rsTimes` 0.1.0 是一个纯 Python 日期时间小工具,提供带时区 `datetime`
|
|
19
|
+
与 Unix 毫秒时间戳的双向转换。没有运行时依赖,要求 Python 3.10 或更新版本。
|
|
20
|
+
当前版本没有 Rust 内核,不承诺性能优势或与其他日期时间库兼容。
|
|
21
|
+
|
|
22
|
+
PyPI 分发名是 `rsTimes`,Python 导入名是 `rstimes`。
|
|
23
|
+
|
|
24
|
+
## 使用
|
|
25
|
+
|
|
26
|
+
首次成功发布到 PyPI 后可安装:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
python -m pip install rsTimes
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from datetime import datetime, timedelta, timezone
|
|
34
|
+
from rstimes import from_unix_ms, to_unix_ms
|
|
35
|
+
|
|
36
|
+
local_epoch = datetime(1970, 1, 1, 8, tzinfo=timezone(timedelta(hours=8)))
|
|
37
|
+
assert to_unix_ms(local_epoch) == 0
|
|
38
|
+
assert from_unix_ms(0) == datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
39
|
+
|
|
40
|
+
before_epoch = datetime(1969, 12, 31, 23, 59, 59, 999999, tzinfo=timezone.utc)
|
|
41
|
+
assert to_unix_ms(before_epoch) == -1
|
|
42
|
+
assert from_unix_ms(-1).isoformat() == "1969-12-31T23:59:59.999000+00:00"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 转换规则
|
|
46
|
+
|
|
47
|
+
- `to_unix_ms(value: datetime) -> int`:输入必须带有效 UTC 偏移,
|
|
48
|
+
转换到 UTC 后用整数计算。微秒余数**向负无穷方向取整**:
|
|
49
|
+
纪元后 999 微秒得到 `0`,纪元前 1 微秒得到 `-1`。
|
|
50
|
+
- `from_unix_ms(value: int) -> datetime`:只接受整数,拒绝 `bool`、
|
|
51
|
+
浮点数、字符串等类型;返回 `timezone.utc` 的带时区时间。
|
|
52
|
+
- 毫秒范围为 `-62135596800000` 到 `253402300799999`,含两端,
|
|
53
|
+
对应 UTC 年份 1–9999。超出范围或时区转换越界抛出 `ValueError`;
|
|
54
|
+
naive datetime 也抛出 `ValueError`,非法输入类型抛出 `TypeError`。
|
|
55
|
+
- 不使用本机时区、不经过浮点时间戳。毫秒整数可以精确往返;
|
|
56
|
+
datetime 的不足 1 毫秒部分在转换时会丢失。
|
|
57
|
+
|
|
58
|
+
## 本地开发与构建
|
|
59
|
+
|
|
60
|
+
以下命令适用于 macOS/Linux;Windows 可用 `.venv\Scripts\python`。
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
python3 -m venv .venv
|
|
64
|
+
.venv/bin/python -m pip install --index-url https://pypi.org/simple -e ".[test]" build twine
|
|
65
|
+
.venv/bin/python -m pytest
|
|
66
|
+
.venv/bin/python -m build
|
|
67
|
+
.venv/bin/python -m twine check --strict dist/*
|
|
68
|
+
|
|
69
|
+
.venv/bin/python -m venv .wheel-test
|
|
70
|
+
.wheel-test/bin/python -m pip install --no-deps dist/*.whl
|
|
71
|
+
.wheel-test/bin/python -I -c 'from rstimes import from_unix_ms, to_unix_ms; assert to_unix_ms(from_unix_ms(-1)) == -1'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
重新构建前清理旧的 `dist/` 内容,避免混入以前的发布文件。
|
|
75
|
+
CI 配置会在 GitHub 的 Ubuntu runner 上使用 Python 3.10 和 3.13 测试、构建、
|
|
76
|
+
检查元数据,并从 wheel 安装验证导入;这些配置不代表已在所有平台完成测试。
|
|
77
|
+
|
|
78
|
+
## 发布前需要填写的信息
|
|
79
|
+
|
|
80
|
+
维护者需要自行填写真实信息:
|
|
81
|
+
|
|
82
|
+
| 项目 | 当前状态 |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| GitHub owner | 待填写本人账号或组织名 |
|
|
85
|
+
| GitHub repository | 待填写实际仓库名 |
|
|
86
|
+
| 许可证 | [MIT](LICENSE),Copyright (c) 2026 Jiefu Chen |
|
|
87
|
+
| 作者与项目 URL | 尚未填写;按实际情况补充,不使用虚构身份或链接 |
|
|
88
|
+
|
|
89
|
+
本项目采用 [MIT 许可证](LICENSE),许可证文本随源码与分发包一同提供。
|
|
90
|
+
|
|
91
|
+
PyPI 将没有功能或空包的占名项目视为无效项目。本版本提供实际的时间转换功能,
|
|
92
|
+
README 和包描述应保持与实际功能一致。
|
|
93
|
+
|
|
94
|
+
## GitHub → PyPI Trusted Publisher
|
|
95
|
+
|
|
96
|
+
本地配置已经准备好,以下持久授权和发布操作由维护者完成:
|
|
97
|
+
|
|
98
|
+
1. 补齐需要的真实元数据;自行把这些文件提交并推送到自己的 GitHub
|
|
99
|
+
仓库。CI 成功后再准备正式 release。
|
|
100
|
+
2. 在该 GitHub 仓库的 Settings → Environments 中创建名为 `pypi` 的 environment。
|
|
101
|
+
如有需要,为它配置审批人和允许发布的 tag 规则。
|
|
102
|
+
3. 若 PyPI 尚无该项目,在自己的 PyPI 账号 Publishing 页面添加 **Pending Publisher**,
|
|
103
|
+
选择 GitHub,准确填写下表。已有项目则在该项目的 Publishing 页面添加 publisher。
|
|
104
|
+
|
|
105
|
+
| PyPI 字段 | 必须填写的值 |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| PyPI Project Name(pending 时) | `rsTimes` |
|
|
108
|
+
| Owner | 实际 GitHub owner,须与仓库所属账号或组织一致 |
|
|
109
|
+
| Repository name | 实际 GitHub 仓库名 |
|
|
110
|
+
| Workflow name | `publish.yml`(文件名,不是 workflow 显示名称) |
|
|
111
|
+
| Environment name | `pypi` |
|
|
112
|
+
|
|
113
|
+
Pending Publisher 在首次成功上传前不会创建项目或预留包名;名称也可能已被占用
|
|
114
|
+
或被 PyPI 保留。首次成功发布后 pending publisher 转为正式 publisher。
|
|
115
|
+
4. 当前 `pyproject.toml` 的版本为 `0.1.0`。在包含上述文件的 commit 上创建 tag
|
|
116
|
+
`v0.1.0`,再发布对应的 GitHub Release。仅推送 tag 或保存 release 草稿不会触发上传。
|
|
117
|
+
5. `.github/workflows/publish.yml` 只响应 `release: published`,检查 tag 与包版本
|
|
118
|
+
精确匹配(必须是 `v` 加版本号),随后测试、构建、检查 sdist/wheel 并验证 wheel。
|
|
119
|
+
只有这些步骤全部成功才会进入发布 job。
|
|
120
|
+
6. 发布 job 使用 `environment: pypi`,仅该 job 有 `id-token: write`。
|
|
121
|
+
它只下载本次运行的构建产物,使用 PyPA 官方 action 的 OIDC 身份发布,
|
|
122
|
+
不需要设置 `PYPI_TOKEN` 或用户名/密码 secret。
|
|
123
|
+
|
|
124
|
+
PyPI 发布是公开且持久的;首次发布前检查 wheel/sdist 内容与真实元数据。
|
|
125
|
+
新版本请先修改 `pyproject.toml`,重新验证,再创建匹配的新 tag/release。
|
|
126
|
+
已经用过的版本和分发文件不能直接覆盖。
|
|
127
|
+
|
|
128
|
+
官方说明:
|
|
129
|
+
|
|
130
|
+
- [Python 项目打包](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
|
|
131
|
+
- [创建 Pending Trusted Publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
|
|
132
|
+
- [通过 Trusted Publisher 发布](https://docs.pypi.org/trusted-publishers/using-a-publisher/)
|
|
133
|
+
- [PyPI 名称政策](https://docs.pypi.org/project-management/name-retention/#invalid-projects)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/rsTimes.egg-info/PKG-INFO
|
|
5
|
+
src/rsTimes.egg-info/SOURCES.txt
|
|
6
|
+
src/rsTimes.egg-info/dependency_links.txt
|
|
7
|
+
src/rsTimes.egg-info/requires.txt
|
|
8
|
+
src/rsTimes.egg-info/top_level.txt
|
|
9
|
+
src/rstimes/__init__.py
|
|
10
|
+
src/rstimes/py.typed
|
|
11
|
+
tests/test_rstimes.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rstimes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Exact conversions between aware datetimes and Unix milliseconds."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
|
|
5
|
+
__all__ = ["to_unix_ms", "from_unix_ms"]
|
|
6
|
+
|
|
7
|
+
_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def to_unix_ms(value: datetime) -> int:
|
|
11
|
+
"""Return Unix milliseconds, flooring any sub-millisecond remainder.
|
|
12
|
+
|
|
13
|
+
``value`` must be timezone-aware and representable in UTC. For example,
|
|
14
|
+
one microsecond before the epoch returns -1, not 0. All arithmetic is
|
|
15
|
+
integer-based; the host's local timezone is never used.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
TypeError: If value is not a datetime.
|
|
19
|
+
ValueError: If value is naive or outside the supported UTC range.
|
|
20
|
+
"""
|
|
21
|
+
if not isinstance(value, datetime):
|
|
22
|
+
raise TypeError("value must be a datetime")
|
|
23
|
+
if value.utcoffset() is None:
|
|
24
|
+
raise ValueError("value must be timezone-aware")
|
|
25
|
+
try:
|
|
26
|
+
delta = value.astimezone(timezone.utc) - _EPOCH
|
|
27
|
+
except OverflowError as exc:
|
|
28
|
+
raise ValueError("value is outside the supported UTC datetime range") from exc
|
|
29
|
+
return (delta.days * 86_400 + delta.seconds) * 1_000 + delta.microseconds // 1_000
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def from_unix_ms(value: int) -> datetime:
|
|
33
|
+
"""Return a UTC-aware datetime for integer Unix milliseconds.
|
|
34
|
+
|
|
35
|
+
Negative values represent instants before 1970-01-01T00:00:00Z.
|
|
36
|
+
The accepted range is -62135596800000 through 253402300799999, inclusive.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
TypeError: If value is not an int, or is a bool.
|
|
40
|
+
ValueError: If value is outside Python's datetime range (years 1–9999).
|
|
41
|
+
"""
|
|
42
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
43
|
+
raise TypeError("value must be an int, not bool or another type")
|
|
44
|
+
try:
|
|
45
|
+
return _EPOCH + timedelta(microseconds=value * 1_000)
|
|
46
|
+
except OverflowError as exc:
|
|
47
|
+
raise ValueError("value is outside the supported UTC datetime range") from exc
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from datetime import date, datetime, timedelta, timezone, tzinfo
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from rstimes import from_unix_ms, to_unix_ms
|
|
7
|
+
|
|
8
|
+
EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
9
|
+
MIN_MS = -62_135_596_800_000
|
|
10
|
+
MAX_MS = 253_402_300_799_999
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.parametrize(
|
|
14
|
+
("microseconds", "milliseconds"),
|
|
15
|
+
[(-1001, -2), (-1000, -1), (-999, -1), (-1, -1),
|
|
16
|
+
(0, 0), (1, 0), (999, 0), (1000, 1), (1001, 1)],
|
|
17
|
+
)
|
|
18
|
+
def test_floor_submillisecond_precision(microseconds, milliseconds):
|
|
19
|
+
assert to_unix_ms(EPOCH + timedelta(microseconds=microseconds)) == milliseconds
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.parametrize(
|
|
23
|
+
"offset", [timedelta(hours=8), timedelta(hours=-7),
|
|
24
|
+
timedelta(hours=5, minutes=30, microseconds=500)],
|
|
25
|
+
)
|
|
26
|
+
def test_offset_datetimes_represent_the_same_instant(offset):
|
|
27
|
+
value = (EPOCH + offset).replace(tzinfo=timezone(offset))
|
|
28
|
+
assert to_unix_ms(value) == 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize("milliseconds", [MIN_MS, -86_400_001, -1001, -1, 0, 1,
|
|
32
|
+
1_700_000_000_123, MAX_MS])
|
|
33
|
+
def test_integer_roundtrip_and_utc_result(milliseconds):
|
|
34
|
+
result = from_unix_ms(milliseconds)
|
|
35
|
+
assert result.tzinfo is timezone.utc
|
|
36
|
+
assert to_unix_ms(result) == milliseconds
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_negative_millisecond_has_expected_date():
|
|
40
|
+
assert from_unix_ms(-1) == datetime(1969, 12, 31, 23, 59, 59, 999000,
|
|
41
|
+
tzinfo=timezone.utc)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_datetime_limits_and_far_future_precision():
|
|
45
|
+
assert from_unix_ms(MIN_MS) == datetime.min.replace(tzinfo=timezone.utc)
|
|
46
|
+
assert from_unix_ms(MAX_MS) == datetime(9999, 12, 31, 23, 59, 59, 999000,
|
|
47
|
+
tzinfo=timezone.utc)
|
|
48
|
+
assert to_unix_ms(datetime.max.replace(tzinfo=timezone.utc)) == MAX_MS
|
|
49
|
+
assert to_unix_ms(datetime(9999, 12, 31, 23, 59, 59, 998999,
|
|
50
|
+
tzinfo=timezone.utc)) == MAX_MS - 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.parametrize("value", [None, True, False, 1, 1.5, "1970-01-01",
|
|
54
|
+
date(1970, 1, 1), object()])
|
|
55
|
+
def test_to_unix_ms_rejects_non_datetime(value):
|
|
56
|
+
with pytest.raises(TypeError):
|
|
57
|
+
to_unix_ms(value)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NoOffset(tzinfo):
|
|
61
|
+
def utcoffset(self, value):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize("value", [datetime(1970, 1, 1),
|
|
66
|
+
datetime(1970, 1, 1, tzinfo=NoOffset())])
|
|
67
|
+
def test_to_unix_ms_rejects_naive_datetime(value):
|
|
68
|
+
with pytest.raises(ValueError, match="timezone-aware"):
|
|
69
|
+
to_unix_ms(value)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.mark.parametrize("value", [None, True, False, 1.0, "1", Decimal("1"),
|
|
73
|
+
EPOCH, object()])
|
|
74
|
+
def test_from_unix_ms_rejects_non_integer_and_bool(value):
|
|
75
|
+
with pytest.raises(TypeError):
|
|
76
|
+
from_unix_ms(value)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.parametrize("value", [MIN_MS - 1, MAX_MS + 1, -(10**100), 10**100])
|
|
80
|
+
def test_from_unix_ms_rejects_out_of_range_integer(value):
|
|
81
|
+
with pytest.raises(ValueError, match="range"):
|
|
82
|
+
from_unix_ms(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.parametrize("value", [
|
|
86
|
+
datetime.min.replace(tzinfo=timezone(timedelta(hours=1))),
|
|
87
|
+
datetime.max.replace(tzinfo=timezone(timedelta(hours=-1))),
|
|
88
|
+
])
|
|
89
|
+
def test_to_unix_ms_rejects_out_of_range_utc_conversion(value):
|
|
90
|
+
with pytest.raises(ValueError, match="range"):
|
|
91
|
+
to_unix_ms(value)
|