betaquant 0.6.0__tar.gz → 0.6.2__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.
Files changed (81) hide show
  1. {betaquant-0.6.0 → betaquant-0.6.2}/Cargo.lock +1 -1
  2. {betaquant-0.6.0 → betaquant-0.6.2}/Cargo.toml +1 -1
  3. {betaquant-0.6.0 → betaquant-0.6.2}/PKG-INFO +59 -1
  4. {betaquant-0.6.0 → betaquant-0.6.2}/README.md +58 -0
  5. {betaquant-0.6.0 → betaquant-0.6.2}/build.rs +7 -0
  6. betaquant-0.6.2/python/betaquant/algo/__init__.py +14 -0
  7. {betaquant-0.6.0 → betaquant-0.6.2}/src/lib.rs +27 -6
  8. {betaquant-0.6.0 → betaquant-0.6.2}/src/license.rs +54 -8
  9. betaquant-0.6.0/python/betaquant/algo/__init__.py +0 -6
  10. {betaquant-0.6.0 → betaquant-0.6.2}/.gitea/builder/Dockerfile +0 -0
  11. {betaquant-0.6.0 → betaquant-0.6.2}/.gitea/builder/README.md +0 -0
  12. {betaquant-0.6.0 → betaquant-0.6.2}/.gitea/workflows/publish.yml +0 -0
  13. {betaquant-0.6.0 → betaquant-0.6.2}/.gitignore +0 -0
  14. {betaquant-0.6.0 → betaquant-0.6.2}/CHANGELOG.md +0 -0
  15. {betaquant-0.6.0 → betaquant-0.6.2}/LICENSE +0 -0
  16. {betaquant-0.6.0 → betaquant-0.6.2}/examples/gtja191/al/__init__.py +0 -0
  17. {betaquant-0.6.0 → betaquant-0.6.2}/examples/gtja191/al/alpha191.py +0 -0
  18. {betaquant-0.6.0 → betaquant-0.6.2}/examples/gtja191/al/alpha191_context.py +0 -0
  19. {betaquant-0.6.0 → betaquant-0.6.2}/examples/gtja191/alpha191.txt +0 -0
  20. {betaquant-0.6.0 → betaquant-0.6.2}/examples/gtja191/main.py +0 -0
  21. {betaquant-0.6.0 → betaquant-0.6.2}/examples/quickstart/full_demo.py +0 -0
  22. {betaquant-0.6.0 → betaquant-0.6.2}/examples/quickstart/rank.py +0 -0
  23. {betaquant-0.6.0 → betaquant-0.6.2}/examples/quickstart/usage.py +0 -0
  24. {betaquant-0.6.0 → betaquant-0.6.2}/examples/quickstart/verify_sumif.py +0 -0
  25. {betaquant-0.6.0 → betaquant-0.6.2}/examples/wq101/al/__init__.py +0 -0
  26. {betaquant-0.6.0 → betaquant-0.6.2}/examples/wq101/al/alpha101.py +0 -0
  27. {betaquant-0.6.0 → betaquant-0.6.2}/examples/wq101/al/alpha101_context.py +0 -0
  28. {betaquant-0.6.0 → betaquant-0.6.2}/examples/wq101/alpha101.txt +0 -0
  29. {betaquant-0.6.0 → betaquant-0.6.2}/examples/wq101/main.py +0 -0
  30. {betaquant-0.6.0 → betaquant-0.6.2}/pyproject.toml +0 -0
  31. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/__init__.py +0 -0
  32. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/algo/algo_gen.py +0 -0
  33. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/algo/manual.py +0 -0
  34. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/algo.md +0 -0
  35. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/context.py +0 -0
  36. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/lang/__init__.py +0 -0
  37. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/lang/__main__.py +0 -0
  38. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/lang/alpha.lark +0 -0
  39. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/lang/parser.py +0 -0
  40. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/lang/to_python.py +0 -0
  41. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/perf.py +0 -0
  42. {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/transforms.py +0 -0
  43. {betaquant-0.6.0 → betaquant-0.6.2}/python/tests/test_grammar.py +0 -0
  44. {betaquant-0.6.0 → betaquant-0.6.2}/python/tests/test_rank.py +0 -0
  45. {betaquant-0.6.0 → betaquant-0.6.2}/python/tests/test_to_python.py +0 -0
  46. {betaquant-0.6.0 → betaquant-0.6.2}/rustfmt.toml +0 -0
  47. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/alpha.rs +0 -0
  48. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/backfill.rs +0 -0
  49. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/context.rs +0 -0
  50. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/cross.rs +0 -0
  51. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/cut.rs +0 -0
  52. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/drawdown.rs +0 -0
  53. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/ema.rs +0 -0
  54. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/entropy.rs +0 -0
  55. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/error.rs +0 -0
  56. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/extremum.rs +0 -0
  57. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/group.rs +0 -0
  58. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/internal.rs +0 -0
  59. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/ma.rs +0 -0
  60. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/misc.rs +0 -0
  61. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/mod.rs +0 -0
  62. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/moments.rs +0 -0
  63. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/neutralize.rs +0 -0
  64. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/quantile.rs +0 -0
  65. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/rank.rs +0 -0
  66. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/returns.rs +0 -0
  67. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/rolling.rs +0 -0
  68. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/series.rs +0 -0
  69. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/sharpe.rs +0 -0
  70. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/slope.rs +0 -0
  71. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/spec.rs +0 -0
  72. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/split.rs +0 -0
  73. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/stats.rs +0 -0
  74. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/stddev.rs +0 -0
  75. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/sum.rs +0 -0
  76. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/topk.rs +0 -0
  77. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/wsum.rs +0 -0
  78. {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/zscore.rs +0 -0
  79. {betaquant-0.6.0 → betaquant-0.6.2}/src/license/public_key.bin +0 -0
  80. {betaquant-0.6.0 → betaquant-0.6.2}/src//347/256/227/345/255/220/350/247/204/345/210/231/345/277/205/350/257/273.md" +0 -0
  81. {betaquant-0.6.0 → betaquant-0.6.2}/tag_release.py +0 -0
@@ -37,7 +37,7 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
37
37
 
38
38
  [[package]]
39
39
  name = "betaquant"
40
- version = "0.6.0"
40
+ version = "0.6.2"
41
41
  dependencies = [
42
42
  "anyhow",
43
43
  "base64",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "betaquant"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  edition = "2024"
5
5
  authors = ["ZhaoJun"]
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: betaquant
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -28,6 +28,64 @@ Project-URL: repository, https://home.zhaojun.com
28
28
  pip install betaquant
29
29
  ```
30
30
 
31
+ ## 授权(License Token)
32
+
33
+ betaquant 的算子受许可证(license)保护,**使用前需持有有效的 license token**。
34
+
35
+
36
+ ### 取得机器指纹(机器绑定用)
37
+
38
+ ```python
39
+ import betaquant
40
+
41
+ machine = betaquant.machine_fingerprint() # 跨 Linux/Windows,机器绑定以它为准
42
+ print(machine)
43
+ ```
44
+
45
+ 把该指纹提交给签发方,换取绑定到本机的 token。
46
+
47
+ ### 启用 token:算子入口自动鉴权(推荐)
48
+
49
+ 调用 `set_license_token` 把 token 设入进程级状态后,**每个算子入口都会自动鉴权**:
50
+ 未授权 / 过期 / 未设 token 时算子直接抛 `ValueError`,无需在每次调用前手写判断。
51
+
52
+
53
+ ```python
54
+ import betaquant
55
+
56
+ token = "<从签发方取得的 license key>"
57
+ betaquant.set_license_token(token) # 启动时一次
58
+
59
+ result = betaquant.ts_ma(data, 3) # 已授权 → 正常计算
60
+ # 若 token 未设置 / 过期 / 该算子不在 allow,betaquant.ts_ma(...) 直接抛 ValueError
61
+ ```
62
+
63
+ > 不调用 `set_license_token` 而直接调算子会抛 `ValueError`。这是与早期版本的关键区别:
64
+ > 鉴权不再依赖调用方自觉判断,而是内嵌在每个算子入口。
65
+
66
+ ### 手动校验(可选)
67
+
68
+ 也可显式验签或单独判断某算子是否授权(不改变进程级 token 状态):
69
+
70
+ ```python
71
+ # 验签 + 校验(失败 / 过期 / 机器不符抛 ValueError),返回声明 dict
72
+ claims = betaquant.verify_license(token, machine=machine)
73
+
74
+ # 判断某算子是否被授权(token 无效 / 过期直接返回 False)
75
+ if betaquant.license_allows(token, "ts_ma", machine=machine):
76
+ ...
77
+ ```
78
+
79
+ 授权语义(**默认拒绝**):算子必须显式出现在 `allow`(或 `allow` 含 `"*"`)才放行;
80
+ `deny` 优先级高于 `allow`。`allow=[]` 表示全部拒绝,并非不限制。
81
+
82
+ > 过期判定使用本机系统时间,离线方案对“改系统时间”无法根治,属已知限制:用短有效期
83
+ > + 定期换发缓解。
84
+
85
+ 更完整的端到端示例(签发 → 启用 token → 算子硬门禁 → 过期续期 → 机器绑定)见
86
+ `encryptor/license/examples/demo.py`。签发方封装见 `encryptor/license`
87
+ (`LicenseService`)。
88
+
31
89
  ## 使用
32
90
 
33
91
  ### 上下文设置
@@ -12,6 +12,64 @@
12
12
  pip install betaquant
13
13
  ```
14
14
 
15
+ ## 授权(License Token)
16
+
17
+ betaquant 的算子受许可证(license)保护,**使用前需持有有效的 license token**。
18
+
19
+
20
+ ### 取得机器指纹(机器绑定用)
21
+
22
+ ```python
23
+ import betaquant
24
+
25
+ machine = betaquant.machine_fingerprint() # 跨 Linux/Windows,机器绑定以它为准
26
+ print(machine)
27
+ ```
28
+
29
+ 把该指纹提交给签发方,换取绑定到本机的 token。
30
+
31
+ ### 启用 token:算子入口自动鉴权(推荐)
32
+
33
+ 调用 `set_license_token` 把 token 设入进程级状态后,**每个算子入口都会自动鉴权**:
34
+ 未授权 / 过期 / 未设 token 时算子直接抛 `ValueError`,无需在每次调用前手写判断。
35
+
36
+
37
+ ```python
38
+ import betaquant
39
+
40
+ token = "<从签发方取得的 license key>"
41
+ betaquant.set_license_token(token) # 启动时一次
42
+
43
+ result = betaquant.ts_ma(data, 3) # 已授权 → 正常计算
44
+ # 若 token 未设置 / 过期 / 该算子不在 allow,betaquant.ts_ma(...) 直接抛 ValueError
45
+ ```
46
+
47
+ > 不调用 `set_license_token` 而直接调算子会抛 `ValueError`。这是与早期版本的关键区别:
48
+ > 鉴权不再依赖调用方自觉判断,而是内嵌在每个算子入口。
49
+
50
+ ### 手动校验(可选)
51
+
52
+ 也可显式验签或单独判断某算子是否授权(不改变进程级 token 状态):
53
+
54
+ ```python
55
+ # 验签 + 校验(失败 / 过期 / 机器不符抛 ValueError),返回声明 dict
56
+ claims = betaquant.verify_license(token, machine=machine)
57
+
58
+ # 判断某算子是否被授权(token 无效 / 过期直接返回 False)
59
+ if betaquant.license_allows(token, "ts_ma", machine=machine):
60
+ ...
61
+ ```
62
+
63
+ 授权语义(**默认拒绝**):算子必须显式出现在 `allow`(或 `allow` 含 `"*"`)才放行;
64
+ `deny` 优先级高于 `allow`。`allow=[]` 表示全部拒绝,并非不限制。
65
+
66
+ > 过期判定使用本机系统时间,离线方案对“改系统时间”无法根治,属已知限制:用短有效期
67
+ > + 定期换发缓解。
68
+
69
+ 更完整的端到端示例(签发 → 启用 token → 算子硬门禁 → 过期续期 → 机器绑定)见
70
+ `encryptor/license/examples/demo.py`。签发方封装见 `encryptor/license`
71
+ (`LicenseService`)。
72
+
15
73
  ## 使用
16
74
 
17
75
  ### 上下文设置
@@ -218,6 +218,13 @@ fn build_py_bindings(functions: &[TaFunc]) -> Result<()> {
218
218
  write!(code, "{}", py_args)?;
219
219
  writeln!(code, " ) -> PyResult<()> {{")?;
220
220
 
221
+ // license 硬门禁:算子入口先做鉴权,未授权 / 过期 / 未设置 token 直接抛 ValueError。
222
+ writeln!(
223
+ code,
224
+ " crate::license::require_for(\"{}\").map_err(|e| PyValueError::new_err(e.to_string()))?;",
225
+ py_func_name
226
+ )?;
227
+
221
228
  writeln!(code, " let ctx = ctx(py);")?;
222
229
 
223
230
  let arrays: Vec<(&String, &TaType)> = func
@@ -0,0 +1,14 @@
1
+ # Copyright 2026 MSD-RS Project LiJia
2
+ # SPDX-License-Identifier: BSD-2-Clause
3
+
4
+ from .manual import ts_ema
5
+ from .algo_gen import *
6
+ from ._algo import (
7
+ set_ctx,
8
+ reset_ctx,
9
+ verify_license,
10
+ license_allows,
11
+ machine_fingerprint,
12
+ set_license_token,
13
+ clear_license_token,
14
+ )
@@ -78,6 +78,8 @@ mod algo_impl {
78
78
  input: &'py Bound<'_, PyAny>,
79
79
  periods: usize,
80
80
  ) -> PyResult<()> {
81
+ crate::license::require_for("ts_ema")
82
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
81
83
  let ctx = ctx(py);
82
84
 
83
85
  if let Some((mut r, input)) = r
@@ -131,17 +133,17 @@ mod license_impl {
131
133
 
132
134
  /// 验签 + 校验 license key,成功返回声明 dict,失败抛 ValueError。
133
135
  ///
136
+ /// 机器绑定自动用本机指纹校验(betaquant 是客户端,永远校验本机)。
134
137
  /// 返回的 dict 含:sub / allow / deny / iat / exp / machine / kid。
135
138
  #[pyfunction]
136
- #[pyo3(signature = (key, now=None, machine=None, check_expiry=true))]
139
+ #[pyo3(signature = (key, now=None, check_expiry=true))]
137
140
  pub fn verify_license<'py>(
138
141
  py: Python<'py>,
139
142
  key: &str,
140
143
  now: Option<i64>,
141
- machine: Option<&str>,
142
144
  check_expiry: bool,
143
145
  ) -> PyResult<Bound<'py, PyDict>> {
144
- let claims = license::verify(key, now, machine, check_expiry).map_err(to_pyerr)?;
146
+ let claims = license::verify(key, now, check_expiry).map_err(to_pyerr)?;
145
147
  let d = PyDict::new(py);
146
148
  d.set_item("sub", claims.sub)?;
147
149
  d.set_item("allow", PyList::new(py, &claims.allow)?)?;
@@ -154,17 +156,18 @@ mod license_impl {
154
156
  }
155
157
 
156
158
  /// 校验 key 后判断某算子是否可用;key 无效 / 过期直接视为不可用(返回 False)。
159
+ ///
160
+ /// 机器绑定自动用本机指纹校验。
157
161
  #[pyfunction]
158
- #[pyo3(signature = (key, operator, now=None, machine=None, check_expiry=true))]
162
+ #[pyo3(signature = (key, operator, now=None, check_expiry=true))]
159
163
  pub fn license_allows<'py>(
160
164
  _py: Python<'py>,
161
165
  key: &str,
162
166
  operator: &str,
163
167
  now: Option<i64>,
164
- machine: Option<&str>,
165
168
  check_expiry: bool,
166
169
  ) -> PyResult<bool> {
167
- match license::verify(key, now, machine, check_expiry) {
170
+ match license::verify(key, now, check_expiry) {
168
171
  Ok(claims) => Ok(claims.allows(operator)),
169
172
  Err(_) => Ok(false),
170
173
  }
@@ -175,6 +178,22 @@ mod license_impl {
175
178
  pub fn machine_fingerprint<'py>(_py: Python<'py>) -> PyResult<String> {
176
179
  Ok(license::machine_fingerprint())
177
180
  }
181
+
182
+ /// 设置进程内 license token:验签通过后所有算子调用自动鉴权。
183
+ ///
184
+ /// 失败抛 ``ValueError``。机器绑定自动用本机指纹校验。
185
+ #[pyfunction]
186
+ pub fn set_license_token<'py>(_py: Python<'py>, key: &str) -> PyResult<()> {
187
+ license::set_token(key).map_err(to_pyerr)?;
188
+ Ok(())
189
+ }
190
+
191
+ /// 清空进程内 license token(主要用于测试 / 释放)。
192
+ #[pyfunction]
193
+ pub fn clear_license_token<'py>(_py: Python<'py>) -> PyResult<()> {
194
+ license::clear_token();
195
+ Ok(())
196
+ }
178
197
  }
179
198
 
180
199
  #[pymodule]
@@ -188,6 +207,8 @@ fn _algo(m: &Bound<'_, PyModule>) -> PyResult<()> {
188
207
  m.add_function(wrap_pyfunction!(license_impl::verify_license, m)?)?;
189
208
  m.add_function(wrap_pyfunction!(license_impl::license_allows, m)?)?;
190
209
  m.add_function(wrap_pyfunction!(license_impl::machine_fingerprint, m)?)?;
210
+ m.add_function(wrap_pyfunction!(license_impl::set_license_token, m)?)?;
211
+ m.add_function(wrap_pyfunction!(license_impl::clear_license_token, m)?)?;
191
212
  algo_impl::register_functions(m)?;
192
213
  Ok(())
193
214
  }
@@ -9,6 +9,7 @@
9
9
  //!
10
10
  //! 验签逻辑放在编译产物里(而非明文 Python),显著抬高 patch / 绕过门槛。
11
11
 
12
+ use std::sync::{LazyLock, RwLock};
12
13
  use std::time::{SystemTime, UNIX_EPOCH};
13
14
 
14
15
  use base64::Engine;
@@ -20,6 +21,11 @@ use sha2::{Digest, Sha256};
20
21
  /// 编译期内嵌的 32 字节原始 Ed25519 公钥。换钥 = 替换此文件 + 重新编译。
21
22
  static PUBLIC_KEY_BYTES: &[u8] = include_bytes!("license/public_key.bin");
22
23
 
24
+ /// 进程内当前生效的 license claims。`set_token` 写入,`require_for` 读取。
25
+ /// 只在二进制内可见 / 可改,Python 侧无法 patch。
26
+ static CURRENT_CLAIMS: LazyLock<RwLock<Option<Claims>>> =
27
+ LazyLock::new(|| RwLock::new(None));
28
+
23
29
  #[derive(Debug, thiserror::Error)]
24
30
  pub enum LicenseError {
25
31
  #[error("license key 格式错误(应为 payload.signature)")]
@@ -38,6 +44,10 @@ pub enum LicenseError {
38
44
  Expired,
39
45
  #[error("license 与当前机器不匹配")]
40
46
  MachineMismatch,
47
+ #[error("license token 未设置(请先调用 set_license_token)")]
48
+ NotSet,
49
+ #[error("算子 {0} 未授权")]
50
+ NotAllowed(String),
41
51
  }
42
52
 
43
53
  /// 反序列化的声明,字段与 Python 端 `License.to_dict()` 对应。
@@ -283,11 +293,11 @@ fn verifying_key() -> Result<VerifyingKey, LicenseError> {
283
293
  /// 验签 + 校验,返回可信 `Claims`。
284
294
  ///
285
295
  /// - `now`:当前 Unix 秒;`None` 取系统时间。便于测试。
286
- /// - `machine`:当前机器指纹;提供且声明绑定了机器时必须匹配。
296
+ /// - 机器绑定:claims 未绑机器(`machine=None`)则不校验;绑了机器则必须与
297
+ /// **本机指纹**一致(betaquant 是客户端,永远校验本机,无需外部传入)。
287
298
  pub fn verify(
288
299
  key: &str,
289
300
  now: Option<i64>,
290
- machine: Option<&str>,
291
301
  check_expiry: bool,
292
302
  ) -> Result<Claims, LicenseError> {
293
303
  let key = key.trim();
@@ -317,9 +327,9 @@ pub fn verify(
317
327
  return Err(LicenseError::Expired);
318
328
  }
319
329
 
320
- // 4) 机器绑定校验
321
- if let (Some(bound), Some(cur)) = (claims.machine.as_deref(), machine) {
322
- if bound != cur {
330
+ // 4) 机器绑定校验:未绑机器不校验;绑了则必须等于本机指纹
331
+ if let Some(bound) = claims.machine.as_deref() {
332
+ if bound != machine_fingerprint() {
323
333
  return Err(LicenseError::MachineMismatch);
324
334
  }
325
335
  }
@@ -327,6 +337,42 @@ pub fn verify(
327
337
  Ok(claims)
328
338
  }
329
339
 
340
+ /// 设置进程内 license token:验签通过则缓存其 claims,供后续算子鉴权使用。
341
+ ///
342
+ /// 机器绑定由 `verify` 自动用本机指纹校验(betaquant 是客户端,永远校验本机)。
343
+ /// 失败会返回错误;调用方应把错误传给 Python 抛 ``ValueError``。
344
+ pub fn set_token(key: &str) -> Result<Claims, LicenseError> {
345
+ let claims = verify(key, None, true)?;
346
+ let mut slot = CURRENT_CLAIMS
347
+ .write()
348
+ .map_err(|_| LicenseError::BadClaims)?;
349
+ *slot = Some(claims.clone());
350
+ Ok(claims)
351
+ }
352
+
353
+ /// 清空进程内 license token(主要用于测试)。
354
+ pub fn clear_token() {
355
+ if let Ok(mut slot) = CURRENT_CLAIMS.write() {
356
+ *slot = None;
357
+ }
358
+ }
359
+
360
+ /// 算子鉴权钩子:每个算子入口调用。无 token / 过期 / 算子未授权都返 Err。
361
+ ///
362
+ /// 这是 Rust 一侧"硬门禁"——校验在编译产物里,Python 改不了。
363
+ pub fn require_for(operator: &str) -> Result<(), LicenseError> {
364
+ let slot = CURRENT_CLAIMS.read().map_err(|_| LicenseError::BadClaims)?;
365
+ let claims = slot.as_ref().ok_or(LicenseError::NotSet)?;
366
+ // 过期保护(带容差):缓存的 claims 也可能跨过 exp。
367
+ if claims.is_expired(now_unix()) {
368
+ return Err(LicenseError::Expired);
369
+ }
370
+ if !claims.allows(operator) {
371
+ return Err(LicenseError::NotAllowed(operator.to_string()));
372
+ }
373
+ Ok(())
374
+ }
375
+
330
376
  #[cfg(test)]
331
377
  mod tests {
332
378
  use super::*;
@@ -334,8 +380,8 @@ mod tests {
334
380
  // 注意:这些测试依赖内嵌公钥与对应私钥签发的 key,集成测试在 Python 端覆盖。
335
381
  #[test]
336
382
  fn rejects_garbage() {
337
- assert!(verify("not-a-key", None, None, true).is_err());
338
- assert!(verify("a.b.c", None, None, true).is_err());
339
- assert!(verify("", None, None, true).is_err());
383
+ assert!(verify("not-a-key", None, true).is_err());
384
+ assert!(verify("a.b.c", None, true).is_err());
385
+ assert!(verify("", None, true).is_err());
340
386
  }
341
387
  }
@@ -1,6 +0,0 @@
1
- # Copyright 2026 MSD-RS Project LiJia
2
- # SPDX-License-Identifier: BSD-2-Clause
3
-
4
- from .manual import ts_ema
5
- from .algo_gen import *
6
- from ._algo import set_ctx, reset_ctx, verify_license, license_allows, machine_fingerprint
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes