ulidx 0.0.1__py3-none-any.whl

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.
ulidx/__init__.py ADDED
@@ -0,0 +1,341 @@
1
+ """
2
+ Author: lqxnjk <lqxnjk@qq.com>
3
+ Created: 2026-10-23 10:06
4
+ Modified: 2026-10-23 10:06
5
+ Python Version: 3.10
6
+ ULID generation and conversion: sortable, monotonic, thread-safe.
7
+
8
+ Standalone package: stdlib only, no internal dependencies.
9
+ """
10
+
11
+ import calendar
12
+ import datetime
13
+ import os
14
+ import struct
15
+ import sys
16
+ import threading
17
+ import time
18
+ import typing
19
+ import uuid
20
+ from collections.abc import Sequence
21
+
22
+ __all__ = ["Ulid"]
23
+
24
+ class Ulid:
25
+ """生成并转换按时间排序的 128 位 ULID 标识符。"""
26
+
27
+ _symbols = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
28
+ _py3 = (sys.version_info[0] >= 3)
29
+ _text_type = str
30
+
31
+ # 单调性状态按线程隔离:每个线程各自持有「上一次熵 / 上一次毫秒时间戳」,
32
+ # 避免把进程级可变状态暴露给所有调用方而互相干扰。
33
+ _state: threading.local = threading.local()
34
+ # 保护「读旧值 -> 比较 -> 写回」临界区,保证同一线程内的单调性承诺不被并发交错破坏
35
+ _lock: threading.Lock = threading.Lock()
36
+
37
+ if _py3:
38
+ _to_binary = bytes
39
+
40
+ _decode_table: typing.ClassVar[list[int]] = [
41
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
42
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
43
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
44
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
45
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01,
46
+ 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF,
47
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
48
+ 0x0F, 0x10, 0x11, 0xFF, 0x12, 0x13, 0xFF, 0x14, 0x15, 0xFF,
49
+ 0x16, 0x17, 0x18, 0x19, 0x1A, 0xFF, 0x1B, 0x1C, 0x1D, 0x1E,
50
+ 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, 0x0B, 0x0C,
51
+ 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0xFF, 0x12, 0x13, 0xFF, 0x14,
52
+ 0x15, 0xFF, 0x16, 0x17, 0x18, 0x19, 0x1A, 0xFF, 0x1B, 0x1C,
53
+ 0x1D, 0x1E, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
54
+ ]
55
+
56
+ @classmethod
57
+ def generate(cls, n_timestamp: float | datetime.datetime | None = None, n_monotonic: bool = False) -> str:
58
+ """生成按时间排序的 26 字符 ULID 字符串。
59
+
60
+ 参数:
61
+ n_timestamp: n_=时间戳。None 使用当前时间;为 datetime 时按 UTC 取秒;
62
+ 为 float 时按 Unix 秒解释。默认 None。
63
+ n_monotonic: n_=是否单调递增。为 True 时若同一毫秒内生成的熵小于上一次,
64
+ 则重新生成以保证严格递增,避免碰撞。
65
+
66
+ 返回:
67
+ str: 26 字符的 Crockford base32 大写 ULID 字符串。
68
+
69
+ 注意:
70
+ ``n_monotonic`` 只在**单个线程内**保证严格递增;各线程各自持有单调状态,
71
+ 不保证跨线程的全局单调。若需要跨线程全局有序,请由调用方自行串行化。
72
+ """
73
+ return cls.by_bytes(cls._generate_bytes(n_timestamp, n_monotonic))
74
+
75
+ @classmethod
76
+ def generate_batch(
77
+ cls,
78
+ n_count: int = 1,
79
+ n_timestamp: float | datetime.datetime | None = None,
80
+ n_monotonic: bool = False,
81
+ ) -> list[str]:
82
+ """批量生成多个 ULID 字符串(共享同一时间戳)。
83
+
84
+ 与 :meth:`generate` 不同,本方法在一次调用内固定时间戳,仅随机部分不同,
85
+ 因此适用于需要同刻批量生成且不依赖逐次递增的场景;内部复用
86
+ :meth:`_generate_bytes`,熵源与单调逻辑和 :meth:`generate` 完全一致。
87
+
88
+ 参数:
89
+ n_count: m_=生成数量,默认 1。
90
+ n_timestamp: n_=时间戳(语义同 :meth:`generate`),默认 None(当前时间)。
91
+ n_monotonic: n_=是否单调递增,默认 False;同样只在单线程内保证严格递增。
92
+
93
+ 返回:
94
+ list[str]: 长度为 n_count 的 ULID 字符串列表。
95
+ """
96
+ if n_timestamp is None:
97
+ _ts_val = time.time()
98
+ elif isinstance(n_timestamp, datetime.datetime):
99
+ _ts_val = float(calendar.timegm(n_timestamp.utctimetuple()))
100
+ else:
101
+ _ts_val = float(n_timestamp)
102
+
103
+ # 复用 _generate_bytes:统一熵源(os.urandom)、统一加锁的单调逻辑,
104
+ # 避免本方法再维护一份平行实现;传同一 _ts_val 保证整批共享同一毫秒时间戳。
105
+ return [cls.by_bytes(cls._generate_bytes(_ts_val, n_monotonic)) for _ in range(n_count)]
106
+
107
+ @classmethod
108
+ def generate_uuid(cls, n_timestamp: float | datetime.datetime | None = None, n_monotonic: bool = False) -> uuid.UUID:
109
+ """生成与 ULID 等价的 :class:`uuid.UUID` 对象。
110
+
111
+ 参数:
112
+ n_timestamp: n_=时间戳(语义同 :meth:`generate`),默认 None。
113
+ n_monotonic: n_=是否单调递增,默认 False。
114
+
115
+ 返回:
116
+ uuid.UUID: 由 ULID 16 字节直接构造的 UUID。
117
+ """
118
+ return uuid.UUID(bytes=cls._generate_bytes(n_timestamp, n_monotonic))
119
+
120
+ @classmethod
121
+ def _next_entropy(cls, m_timestamp: int, n_monotonic: bool) -> bytes:
122
+ """在锁内生成当前 ULID 的 10 字节熵,并维护线程级单调状态。
123
+
124
+ 参数:
125
+ m_timestamp: m_=当前 ULID 的毫秒时间戳,用于判断是否与上一次同毫秒。
126
+ n_monotonic: m_=是否单调递增;为 True 且与上一次同毫秒时,
127
+ 重新取样直到熵不小于上一次,保证同线程内严格递增。
128
+
129
+ 返回:
130
+ bytes: 10 字节随机熵(统一取 :func:`os.urandom` 密码学随机源)。
131
+
132
+ 注意:
133
+ 状态存放在 :data:`_state`(``threading.local``),因此单调性仅限单线程;
134
+ 读旧值、比较与写回都在 :data:`_lock` 内完成,保证临界区原子。
135
+ """
136
+ _state = cls._state
137
+ with cls._lock:
138
+ _prev = getattr(_state, "entropy", None)
139
+ _prev_timestamp = getattr(_state, "timestamp", None)
140
+ _entropy = os.urandom(10)
141
+ if n_monotonic and _prev is not None and _prev_timestamp == m_timestamp:
142
+ while _entropy < _prev:
143
+ _entropy = os.urandom(10)
144
+ _state.entropy = _entropy
145
+ _state.timestamp = m_timestamp
146
+ return _entropy
147
+
148
+ @classmethod
149
+ def _generate_bytes(cls, n_timestamp: float | datetime.datetime | None = None, n_monotonic: bool = False) -> bytes:
150
+ """生成 ULID 字节(内部方法)。
151
+
152
+ 参数:
153
+ n_timestamp: n_=时间戳,语义同 :meth:`generate`,默认 None(当前时间)。
154
+ n_monotonic: n_=是否单调递增,默认 False(仅单线程内保证严格递增)。
155
+
156
+ 返回:
157
+ bytes: 16 字节 ULID(6 字节毫秒时间戳 + 10 字节随机熵)。
158
+ """
159
+ if n_timestamp is None:
160
+ _ts_val = time.time()
161
+ elif isinstance(n_timestamp, datetime.datetime):
162
+ _ts_val = float(calendar.timegm(n_timestamp.utctimetuple()))
163
+ else:
164
+ _ts_val = float(n_timestamp)
165
+
166
+ _ts = int(_ts_val * 1000.0)
167
+ _ts_bytes: bytes = struct.pack(b"!Q", _ts)[2:]
168
+ _entropy = cls._next_entropy(_ts, n_monotonic)
169
+ return _ts_bytes + _entropy
170
+
171
+ @classmethod
172
+ def by_bytes(cls, m_binary: bytes | Sequence[int]) -> str:
173
+ """
174
+ 将 16 字节二进制数据编码为 26 字符的 base32 字符串
175
+ :param m_binary: 字节串或字节列表
176
+ :return: 26 字符的 ASCII 字符串
177
+ :rtype: str
178
+ """
179
+ assert len(m_binary) == 16
180
+
181
+ _symbols = cls._symbols
182
+ return "".join(
183
+ [
184
+ _symbols[(m_binary[0] & 224) >> 5],
185
+ _symbols[m_binary[0] & 31],
186
+ _symbols[(m_binary[1] & 248) >> 3],
187
+ _symbols[((m_binary[1] & 7) << 2) | ((m_binary[2] & 192) >> 6)],
188
+ _symbols[(m_binary[2] & 62) >> 1],
189
+ _symbols[((m_binary[2] & 1) << 4) | ((m_binary[3] & 240) >> 4)],
190
+ _symbols[((m_binary[3] & 15) << 1) | ((m_binary[4] & 128) >> 7)],
191
+ _symbols[(m_binary[4] & 124) >> 2],
192
+ _symbols[((m_binary[4] & 3) << 3) | ((m_binary[5] & 224) >> 5)],
193
+ _symbols[m_binary[5] & 31],
194
+ _symbols[(m_binary[6] & 248) >> 3],
195
+ _symbols[((m_binary[6] & 7) << 2) | ((m_binary[7] & 192) >> 6)],
196
+ _symbols[(m_binary[7] & 62) >> 1],
197
+ _symbols[((m_binary[7] & 1) << 4) | ((m_binary[8] & 240) >> 4)],
198
+ _symbols[((m_binary[8] & 15) << 1) | ((m_binary[9] & 128) >> 7)],
199
+ _symbols[(m_binary[9] & 124) >> 2],
200
+ _symbols[((m_binary[9] & 3) << 3) | ((m_binary[10] & 224) >> 5)],
201
+ _symbols[m_binary[10] & 31],
202
+ _symbols[(m_binary[11] & 248) >> 3],
203
+ _symbols[((m_binary[11] & 7) << 2) | ((m_binary[12] & 192) >> 6)],
204
+ _symbols[(m_binary[12] & 62) >> 1],
205
+ _symbols[((m_binary[12] & 1) << 4) | ((m_binary[13] & 240) >> 4)],
206
+ _symbols[((m_binary[13] & 15) << 1) | ((m_binary[14] & 128) >> 7)],
207
+ _symbols[(m_binary[14] & 124) >> 2],
208
+ _symbols[((m_binary[14] & 3) << 3) | ((m_binary[15] & 224) >> 5)],
209
+ _symbols[m_binary[15] & 31],
210
+ ]
211
+ )
212
+
213
+ @classmethod
214
+ def to_bytes(cls, m_ulid: str) -> bytes:
215
+ """将 26 字符 ULID 字符串解码为 16 字节。
216
+
217
+ 参数:
218
+ m_ulid: m_=待解码的 ULID 字符串。
219
+
220
+ 返回:
221
+ bytes: 16 字节的二进制表示。
222
+
223
+ 异常:
224
+ ValueError: 长度非 26、首字符超出 '0'-'7' 范围或包含非法 base32 字符时抛出。
225
+ """
226
+ if len(m_ulid) != 26:
227
+ raise ValueError(f"base32 ulid is {len(m_ulid)} characters long, expected 26")
228
+ if not all(c in cls._symbols for c in m_ulid):
229
+ raise ValueError("invalid characters in base32 ulid")
230
+
231
+ _b = [ord(c) for c in m_ulid]
232
+ if _b[0] < 48 or _b[0] > 55:
233
+ raise ValueError(f"base32 ulid is out of range (starts with {m_ulid[0]}; accepted are 01234567)")
234
+
235
+ _tab = cls._decode_table
236
+ _binary = [(c & 0xFF) for c in [
237
+ ((_tab[_b[0]] << 5) | _tab[_b[1]]),
238
+ ((_tab[_b[2]] << 3) | (_tab[_b[3]] >> 2)),
239
+ ((_tab[_b[3]] << 6) | (_tab[_b[4]] << 1) | (_tab[_b[5]] >> 4)),
240
+ ((_tab[_b[5]] << 4) | (_tab[_b[6]] >> 1)),
241
+ ((_tab[_b[6]] << 7) | (_tab[_b[7]] << 2) | (_tab[_b[8]] >> 3)),
242
+ ((_tab[_b[8]] << 5) | _tab[_b[9]]),
243
+ ((_tab[_b[10]] << 3) | (_tab[_b[11]] >> 2)),
244
+ ((_tab[_b[11]] << 6) | (_tab[_b[12]] << 1) | (_tab[_b[13]] >> 4)),
245
+ ((_tab[_b[13]] << 4) | (_tab[_b[14]] >> 1)),
246
+ ((_tab[_b[14]] << 7) | (_tab[_b[15]] << 2) | (_tab[_b[16]] >> 3)),
247
+ ((_tab[_b[16]] << 5) | _tab[_b[17]]),
248
+ ((_tab[_b[18]] << 3) | _tab[_b[19]] >> 2),
249
+ ((_tab[_b[19]] << 6) | (_tab[_b[20]] << 1) | (_tab[_b[21]] >> 4)),
250
+ ((_tab[_b[21]] << 4) | (_tab[_b[22]] >> 1)),
251
+ ((_tab[_b[22]] << 7) | (_tab[_b[23]] << 2) | (_tab[_b[24]] >> 3)),
252
+ ((_tab[_b[24]] << 5) | _tab[_b[25]]),
253
+ ]]
254
+ return bytes(_binary)
255
+
256
+ @classmethod
257
+ def to_binary(cls, m_ulid: str | bytes | bytearray | uuid.UUID) -> bytes | bytearray:
258
+ """将多种表示形式统一转换为 16 字节二进制。
259
+
260
+ 参数:
261
+ m_ulid: m_=ULID 字符串(26 字符)、bytes/bytearray(16 字节)或
262
+ :class:`uuid.UUID` 对象。
263
+
264
+ 返回:
265
+ bytes | bytearray: 对应的 16 字节二进制(输入为 bytearray 时原样返回)。
266
+
267
+ 异常:
268
+ ValueError: 输入无法识别为有效 ULID(长度/类型不符)时抛出。
269
+ """
270
+ if isinstance(m_ulid, uuid.UUID):
271
+ return m_ulid.bytes
272
+ if isinstance(m_ulid, (cls._text_type, bytes)) and len(m_ulid) == 26:
273
+ _str_val = m_ulid.decode("ascii") if isinstance(m_ulid, bytes) else m_ulid
274
+ return cls.to_bytes(_str_val)
275
+ if isinstance(m_ulid, (bytes, bytearray)) and len(m_ulid) == 16:
276
+ return m_ulid
277
+ raise ValueError(f"can not convert ulid {m_ulid!r} to binary")
278
+
279
+ @classmethod
280
+ def to_timestamp(cls, m_ulid: str | bytes | bytearray | uuid.UUID) -> float:
281
+ """从 ULID 提取其时间分量(UTC 毫秒时间戳的秒值)。
282
+
283
+ 参数:
284
+ m_ulid: m_=任意合法的 ULID 表示(字符串/字节/UUID)。
285
+
286
+ 返回:
287
+ float: Unix 秒(含毫秒小数)时间戳。
288
+
289
+ 异常:
290
+ ValueError: 输入非有效 ULID 时由 :meth:`to_binary` 抛出。
291
+ """
292
+ _ts_bytes = bytes(cls.to_binary(m_ulid)[:6])
293
+ _ts_bytes = b"\0\0" + _ts_bytes
294
+ assert len(_ts_bytes) == 8
295
+ _res: tuple[int, ...] = struct.unpack(b"!Q", _ts_bytes)
296
+ return float(_res[0] / 1000.)
297
+
298
+ @classmethod
299
+ def to_time(cls, m_ulid: str | bytes | bytearray | uuid.UUID, n_hour_offset: int = 8) -> datetime.datetime:
300
+ """从 ULID 提取时间并转换为带时区的 :class:`datetime.datetime`。
301
+
302
+ 参数:
303
+ m_ulid: m_=任意合法的 ULID 表示(字符串/字节/UUID)。
304
+ n_hour_offset: n_=目标时区相对 UTC 的小时偏移,默认 +8(中国标准时间)。
305
+
306
+ 返回:
307
+ datetime.datetime: 已附时区信息的本地化时间。
308
+
309
+ 注意:
310
+ 内部先按 UTC 解释时间戳再换算到目标时区,不依赖运行机器的本地时区。
311
+ """
312
+ # to_timestamp 返回的是 UTC 秒:先按 UTC 解释,再换算到目标时区,
313
+ # 避免依赖运行机器的本地时区(旧实现在非 UTC+8 环境会整体偏移)
314
+ _timestamp = cls.to_timestamp(m_ulid)
315
+ _tz = datetime.timezone(datetime.timedelta(hours=n_hour_offset))
316
+ return datetime.datetime.fromtimestamp(_timestamp, datetime.timezone.utc).astimezone(_tz)
317
+
318
+ @classmethod
319
+ def to_uuid(cls, m_ulid: str | bytes | bytearray | uuid.UUID) -> uuid.UUID:
320
+ """将任意 ULID 表示转换为 :class:`uuid.UUID` 对象。
321
+
322
+ 参数:
323
+ m_ulid: m_=ULID 字符串(26 字符)、16 字节或 UUID。
324
+
325
+ 返回:
326
+ uuid.UUID: 由 ULID 二进制构造的 UUID。
327
+ """
328
+ return uuid.UUID(bytes=bytes(cls.to_binary(m_ulid)))
329
+
330
+ @classmethod
331
+ def from_uuid(cls, m_uuid: uuid.UUID) -> str:
332
+ """将 :class:`uuid.UUID` 转换为对应的 ULID 字符串。
333
+
334
+ 参数:
335
+ m_uuid: m_=源 UUID 对象。
336
+
337
+ 返回:
338
+ str: 由其 16 字节编码得到的 26 字符 ULID 字符串。
339
+ """
340
+ return cls.by_bytes(m_uuid.bytes)
341
+
ulidx/py.typed ADDED
File without changes
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.5
2
+ Name: ulidx
3
+ Version: 0.0.1
4
+ Summary: ULID generation and conversion: sortable, monotonic, thread-safe.
5
+ Project-URL: Homepage, https://github.com/lqxnjk/ulidx
6
+ Project-URL: Source, https://github.com/lqxnjk/ulidx
7
+ Author: lqxnjk
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: id,identifier,monotonic,sortable,ulid,uuid
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+
22
+ # ulidx
23
+
24
+ ULID generation and conversion. A ULID is a 128-bit identifier that is both
25
+ **lexicographically sortable by creation time** and **random enough to be used
26
+ as a primary key** — 26 characters in Crockford base32, no hyphens.
27
+
28
+ ## Install
29
+
30
+ ```console
31
+ pip install ulidx
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ from ulidx import Ulid
38
+
39
+ # plain generation
40
+ Ulid.generate() # '01J8Z9K2M3N4P5Q6R7S8T9V0WX'
41
+
42
+ # guaranteed strictly increasing within a process, even in the same millisecond
43
+ Ulid.generate(n_monotonic=True)
44
+
45
+ # pin the timestamp (aware or naive datetime, or epoch seconds)
46
+ Ulid.generate(n_timestamp=1700000000)
47
+
48
+ # bulk
49
+ Ulid.generate_batch(1000)
50
+
51
+ # back and forth
52
+ value = Ulid.generate()
53
+ Ulid.to_timestamp(value) # float, epoch seconds
54
+ Ulid.to_time(value) # datetime in UTC+8
55
+ Ulid.to_bytes(value) # 16 raw bytes
56
+ Ulid.by_bytes(Ulid.to_bytes(value)) # round trip
57
+ Ulid.to_uuid(value) # uuid.UUID
58
+ Ulid.from_uuid(some_uuid) # 26-char str
59
+ ```
60
+
61
+ ## API
62
+
63
+ | Method | Description |
64
+ | --- | --- |
65
+ | `generate(ts=None, monotonic=False)` | New 26-character ULID |
66
+ | `generate_batch(count, ...)` | List of ULIDs |
67
+ | `generate_uuid(ts=None, ...)` | New ULID as `uuid.UUID` |
68
+ | `by_bytes(data)` | 16 bytes to ULID string |
69
+ | `to_bytes(ulid)` | ULID (str / bytes / UUID) to 16 bytes |
70
+ | `to_timestamp(ulid)` | Epoch seconds encoded in the ULID |
71
+ | `to_time(ulid, hour_offset=8)` | `datetime` in the given UTC offset |
72
+ | `to_uuid(ulid)` / `from_uuid(uuid)` | Convert between ULID and `uuid.UUID` |
73
+
74
+ ## Monotonicity
75
+
76
+ `n_monotonic=True` keeps the generator state per thread, so concurrent threads
77
+ never share a counter. Within a thread, ULIDs produced in the same millisecond
78
+ strictly increase, which keeps sorting stable when several IDs are created in
79
+ one tick.
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,6 @@
1
+ ulidx/__init__.py,sha256=9cLR9qBo3VU76-yDIxiDTyynv_R2QpQ-rcjf7Ba5hhA,15241
2
+ ulidx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ ulidx-0.0.1.dist-info/METADATA,sha256=rCA15a7C_qCKSWn7wsL_FecX27Y96yh0Ip3p6SG112k,2648
4
+ ulidx-0.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
5
+ ulidx-0.0.1.dist-info/licenses/LICENSE,sha256=Q2c07NeDARb1XSKFLKAaKA8pCiR_C4baR6PRyCYj-t8,1063
6
+ ulidx-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lqxnjk
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.