uplid 1.0.0__py3-none-any.whl → 1.1.0__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.
- uplid/__init__.py +2 -2
- uplid/sqlalchemy.py +205 -0
- uplid/uplid.py +14 -1
- uplid-1.1.0.dist-info/METADATA +223 -0
- uplid-1.1.0.dist-info/RECORD +7 -0
- uplid-1.0.0.dist-info/METADATA +0 -159
- uplid-1.0.0.dist-info/RECORD +0 -6
- {uplid-1.0.0.dist-info → uplid-1.1.0.dist-info}/WHEEL +0 -0
uplid/__init__.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from uplid.uplid import UPLID, UPLIDError, UPLIDType, factory, parse
|
|
5
|
+
from uplid.uplid import UPLID, UPLIDError, UPLIDType, _get_prefix, factory, parse
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
__all__ = ["UPLID", "UPLIDError", "UPLIDType", "factory", "parse"]
|
|
8
|
+
__all__ = ["UPLID", "UPLIDError", "UPLIDType", "_get_prefix", "factory", "parse"]
|
uplid/sqlalchemy.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""SQLAlchemy integration for UPLID.
|
|
2
|
+
|
|
3
|
+
Provides a TypeDecorator and helper for using UPLIDs as typed columns
|
|
4
|
+
that store as TEXT in the database.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from typing import Literal
|
|
8
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped
|
|
9
|
+
from uplid import UPLID
|
|
10
|
+
from uplid.sqlalchemy import uplid_column
|
|
11
|
+
|
|
12
|
+
UserId = UPLID[Literal["usr"]]
|
|
13
|
+
OrgId = UPLID[Literal["org"]]
|
|
14
|
+
|
|
15
|
+
class Base(DeclarativeBase):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class User(Base):
|
|
19
|
+
__tablename__ = "users"
|
|
20
|
+
|
|
21
|
+
id: Mapped[UserId] = uplid_column(UserId, primary_key=True)
|
|
22
|
+
org_id: Mapped[OrgId | None] = uplid_column(OrgId)
|
|
23
|
+
name: Mapped[str]
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import TYPE_CHECKING, Any, TypedDict, Unpack, cast
|
|
29
|
+
|
|
30
|
+
from sqlalchemy import Text
|
|
31
|
+
from sqlalchemy.orm import mapped_column
|
|
32
|
+
from sqlalchemy.types import TypeDecorator
|
|
33
|
+
|
|
34
|
+
from uplid import UPLID, UPLIDError, UPLIDType, _get_prefix
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Callable
|
|
39
|
+
|
|
40
|
+
from sqlalchemy.engine import Dialect
|
|
41
|
+
from sqlalchemy.orm import MappedColumn
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UPLIDColumnKwargs(TypedDict, total=False):
|
|
45
|
+
"""Keyword arguments for uplid_column, matching mapped_column's common options."""
|
|
46
|
+
|
|
47
|
+
primary_key: bool
|
|
48
|
+
nullable: bool
|
|
49
|
+
default: object
|
|
50
|
+
default_factory: Callable[[], object]
|
|
51
|
+
index: bool
|
|
52
|
+
unique: bool
|
|
53
|
+
insert_default: object
|
|
54
|
+
onupdate: object
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class UPLIDColumn(TypeDecorator[UPLIDType]):
|
|
58
|
+
"""SQLAlchemy TypeDecorator for UPLID storage as TEXT.
|
|
59
|
+
|
|
60
|
+
Automatically serializes UPLID objects to strings on write
|
|
61
|
+
and deserializes back to UPLID objects on read.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
prefix: The expected prefix for UPLIDs in this column.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
id: Mapped[UserId] = mapped_column(UPLIDColumn("usr"), primary_key=True)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
impl = Text
|
|
71
|
+
cache_ok = True
|
|
72
|
+
|
|
73
|
+
def __init__(self, prefix: str) -> None:
|
|
74
|
+
"""Initialize with the expected UPLID prefix."""
|
|
75
|
+
self.prefix = prefix
|
|
76
|
+
super().__init__()
|
|
77
|
+
|
|
78
|
+
def process_bind_param(
|
|
79
|
+
self,
|
|
80
|
+
value: UPLIDType | str | None,
|
|
81
|
+
dialect: Dialect, # noqa: ARG002
|
|
82
|
+
) -> str | None:
|
|
83
|
+
"""Convert UPLID to string for database storage.
|
|
84
|
+
|
|
85
|
+
Validates that strings have the correct prefix before storing.
|
|
86
|
+
This catches prefix mismatches at write time rather than read time.
|
|
87
|
+
"""
|
|
88
|
+
if value is None:
|
|
89
|
+
return None
|
|
90
|
+
if isinstance(value, UPLIDType):
|
|
91
|
+
if value.prefix != self.prefix:
|
|
92
|
+
msg = f"Expected prefix {self.prefix!r}, got {value.prefix!r}"
|
|
93
|
+
raise ValueError(msg)
|
|
94
|
+
return str(value)
|
|
95
|
+
# Validate string format and prefix before storing
|
|
96
|
+
if isinstance(value, str):
|
|
97
|
+
UPLID.from_string(value, self.prefix) # Raises UPLIDError if invalid
|
|
98
|
+
return value
|
|
99
|
+
return value # pragma: no cover
|
|
100
|
+
|
|
101
|
+
def process_result_value(
|
|
102
|
+
self,
|
|
103
|
+
value: str | None,
|
|
104
|
+
dialect: Dialect, # noqa: ARG002
|
|
105
|
+
) -> UPLIDType | None:
|
|
106
|
+
"""Convert database string to UPLID object."""
|
|
107
|
+
if value is None:
|
|
108
|
+
return None
|
|
109
|
+
return UPLID.from_string(value, self.prefix)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _extract_prefix[T](uplid_type: type[T]) -> str:
|
|
113
|
+
"""Extract prefix from a parameterized UPLID type like UPLID[Literal["usr"]].
|
|
114
|
+
|
|
115
|
+
Wraps _get_prefix to convert UPLIDError to TypeError for SQLAlchemy context.
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
return _get_prefix(uplid_type) # type: ignore[arg-type]
|
|
119
|
+
except UPLIDError as e:
|
|
120
|
+
raise TypeError(str(e)) from e
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def uplid_column[T](
|
|
124
|
+
uplid_type: type[T],
|
|
125
|
+
**kwargs: Unpack[UPLIDColumnKwargs],
|
|
126
|
+
) -> MappedColumn[T]:
|
|
127
|
+
"""Create a mapped_column for a UPLID type (pure SQLAlchemy).
|
|
128
|
+
|
|
129
|
+
Infers the prefix from the type parameter, so you don't need to
|
|
130
|
+
specify it twice.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
uplid_type: A parameterized UPLID type like UPLID[Literal["usr"]].
|
|
134
|
+
**kwargs: Additional arguments passed to mapped_column.
|
|
135
|
+
Supports: primary_key, nullable, default, default_factory,
|
|
136
|
+
index, unique, insert_default, onupdate.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A mapped_column configured with the appropriate UPLIDColumn.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
UserId = UPLID[Literal["usr"]]
|
|
143
|
+
OrgId = UPLID[Literal["org"]]
|
|
144
|
+
|
|
145
|
+
class User(Base):
|
|
146
|
+
__tablename__ = "users"
|
|
147
|
+
|
|
148
|
+
id: Mapped[UserId] = uplid_column(UserId, primary_key=True)
|
|
149
|
+
org_id: Mapped[OrgId | None] = uplid_column(OrgId)
|
|
150
|
+
"""
|
|
151
|
+
prefix = _extract_prefix(uplid_type)
|
|
152
|
+
return mapped_column(UPLIDColumn(prefix), **kwargs)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class UPLIDFieldKwargs(TypedDict, total=False):
|
|
156
|
+
"""Keyword arguments for uplid_field, matching SQLModel Field's common options."""
|
|
157
|
+
|
|
158
|
+
default: object
|
|
159
|
+
default_factory: Callable[[], object]
|
|
160
|
+
primary_key: bool
|
|
161
|
+
nullable: bool
|
|
162
|
+
index: bool
|
|
163
|
+
unique: bool
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def uplid_field[T](
|
|
167
|
+
uplid_type: type[T],
|
|
168
|
+
**kwargs: Unpack[UPLIDFieldKwargs],
|
|
169
|
+
) -> Any: # noqa: ANN401 - return type matches SQLModel's Field
|
|
170
|
+
"""Create a SQLModel Field for a UPLID type.
|
|
171
|
+
|
|
172
|
+
Infers the prefix from the type parameter and configures sa_type
|
|
173
|
+
automatically.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
uplid_type: A parameterized UPLID type like UPLID[Literal["usr"]].
|
|
177
|
+
**kwargs: Additional arguments passed to Field.
|
|
178
|
+
Supports: default, default_factory, primary_key, index, unique.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
A SQLModel Field configured with the appropriate UPLIDColumn.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
from sqlmodel import SQLModel
|
|
185
|
+
from uplid import UPLID, factory
|
|
186
|
+
from uplid.sqlalchemy import uplid_field
|
|
187
|
+
|
|
188
|
+
UserId = UPLID[Literal["usr"]]
|
|
189
|
+
UserIdFactory = factory(UserId)
|
|
190
|
+
|
|
191
|
+
class User(SQLModel, table=True):
|
|
192
|
+
id: UserId = uplid_field(UserId, default_factory=UserIdFactory, primary_key=True)
|
|
193
|
+
org_id: OrgId | None = uplid_field(OrgId, default=None)
|
|
194
|
+
"""
|
|
195
|
+
# Import here to avoid hard dependency on sqlmodel
|
|
196
|
+
from sqlmodel import Field
|
|
197
|
+
|
|
198
|
+
prefix = _extract_prefix(uplid_type)
|
|
199
|
+
# SQLModel's sa_type is incorrectly typed as type[Any] but accepts TypeEngine instances.
|
|
200
|
+
# Use cast to satisfy the type checker until SQLModel fixes their stubs.
|
|
201
|
+
sa_type = cast("type[Any]", UPLIDColumn(prefix))
|
|
202
|
+
return Field(sa_type=sa_type, **kwargs)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
__all__ = ["UPLIDColumn", "uplid_column", "uplid_field"]
|
uplid/uplid.py
CHANGED
|
@@ -234,24 +234,28 @@ class UPLID[PREFIX: LiteralString]:
|
|
|
234
234
|
def __lt__(self, other: object) -> bool:
|
|
235
235
|
"""Compare for sorting (by prefix, then by uid)."""
|
|
236
236
|
if isinstance(other, UPLID):
|
|
237
|
+
# type: ignore needed because UUID comparison is not recognized by type checkers
|
|
237
238
|
return (self._prefix, self._uid) < (other._prefix, other._uid) # type: ignore[operator]
|
|
238
239
|
return NotImplemented
|
|
239
240
|
|
|
240
241
|
def __le__(self, other: object) -> bool:
|
|
241
242
|
"""Compare for sorting (by prefix, then by uid)."""
|
|
242
243
|
if isinstance(other, UPLID):
|
|
244
|
+
# type: ignore needed because UUID comparison is not recognized by type checkers
|
|
243
245
|
return (self._prefix, self._uid) <= (other._prefix, other._uid) # type: ignore[operator]
|
|
244
246
|
return NotImplemented
|
|
245
247
|
|
|
246
248
|
def __gt__(self, other: object) -> bool:
|
|
247
249
|
"""Compare for sorting (by prefix, then by uid)."""
|
|
248
250
|
if isinstance(other, UPLID):
|
|
251
|
+
# type: ignore needed because UUID comparison is not recognized by type checkers
|
|
249
252
|
return (self._prefix, self._uid) > (other._prefix, other._uid) # type: ignore[operator]
|
|
250
253
|
return NotImplemented
|
|
251
254
|
|
|
252
255
|
def __ge__(self, other: object) -> bool:
|
|
253
256
|
"""Compare for sorting (by prefix, then by uid)."""
|
|
254
257
|
if isinstance(other, UPLID):
|
|
258
|
+
# type: ignore needed because UUID comparison is not recognized by type checkers
|
|
255
259
|
return (self._prefix, self._uid) >= (other._prefix, other._uid) # type: ignore[operator]
|
|
256
260
|
return NotImplemented
|
|
257
261
|
|
|
@@ -282,6 +286,14 @@ class UPLID[PREFIX: LiteralString]:
|
|
|
282
286
|
UPLIDError: If the prefix is not valid snake_case.
|
|
283
287
|
"""
|
|
284
288
|
_validate_prefix(prefix)
|
|
289
|
+
return cls._generate_unchecked(prefix)
|
|
290
|
+
|
|
291
|
+
@classmethod
|
|
292
|
+
def _generate_unchecked(cls, prefix: PREFIX) -> Self:
|
|
293
|
+
"""Generate a new UPLID without validating the prefix.
|
|
294
|
+
|
|
295
|
+
Internal method for use when prefix has already been validated.
|
|
296
|
+
"""
|
|
285
297
|
instance = cls.__new__(cls)
|
|
286
298
|
instance._prefix = prefix # noqa: SLF001
|
|
287
299
|
instance._uid = uuid7() # noqa: SLF001
|
|
@@ -421,9 +433,10 @@ def factory[PREFIX: LiteralString](
|
|
|
421
433
|
id: UserId = Field(default_factory=factory(UserId))
|
|
422
434
|
"""
|
|
423
435
|
prefix = _get_prefix(uplid_type)
|
|
436
|
+
_validate_prefix(prefix) # Validate once at factory creation
|
|
424
437
|
|
|
425
438
|
def _factory() -> UPLID[PREFIX]:
|
|
426
|
-
return UPLID.
|
|
439
|
+
return UPLID._generate_unchecked(prefix) # noqa: SLF001
|
|
427
440
|
|
|
428
441
|
return _factory
|
|
429
442
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uplid
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Universal Prefixed Literal IDs - type-safe, human-readable identifiers
|
|
5
|
+
Keywords: uuid,id,identifier,pydantic,type-safe,uuid7
|
|
6
|
+
Author: ZVS
|
|
7
|
+
Author-email: ZVS <zvs@daswolf.dev>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Framework :: Pydantic :: 2
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Dist: pydantic>=2.10
|
|
17
|
+
Requires-Python: >=3.14
|
|
18
|
+
Project-URL: Homepage, https://github.com/zvsdev/uplid
|
|
19
|
+
Project-URL: Repository, https://github.com/zvsdev/uplid
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# UPLID
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
██╗ ██╗██████╗ ██╗ ██╗██████╗
|
|
26
|
+
██║ ██║██╔══██╗██║ ██║██╔══██╗
|
|
27
|
+
██║ ██║██████╔╝██║ ██║██║ ██║
|
|
28
|
+
██║ ██║██╔═══╝ ██║ ██║██║ ██║
|
|
29
|
+
╚██████╔╝██║ ███████╗██║██████╔╝
|
|
30
|
+
╚═════╝ ╚═╝ ╚══════╝╚═╝╚═════╝
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Stripe-style IDs for Python.**
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# Before: WTF is this?
|
|
37
|
+
"550e8400-e29b-41d4-a716-446655440000"
|
|
38
|
+
|
|
39
|
+
# After: It's a user.
|
|
40
|
+
"usr_0M3xL9kQ7vR2nP5wY1jZ4c"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
[](https://github.com/zvsdev/uplid/actions/workflows/ci.yml)
|
|
44
|
+
[](https://pypi.org/project/uplid/)
|
|
45
|
+
[](https://pypi.org/project/uplid/)
|
|
46
|
+
[](https://github.com/zvsdev/uplid)
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install uplid
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requires Python 3.14+ and Pydantic 2.10+.
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
>>> from uplid import UPLID
|
|
60
|
+
>>> UPLID.generate("usr")
|
|
61
|
+
usr_0M3xL9kQ7vR2nP5wY1jZ4c
|
|
62
|
+
>>> UPLID.generate("ord")
|
|
63
|
+
ord_7x9KmNpQrStUvWxYz012Ab
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Why UPLID?
|
|
67
|
+
|
|
68
|
+
**Debuggable** - See `usr_` in your logs and instantly know it's a user, not an order, not a session, not a mystery.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
# Your logs now:
|
|
72
|
+
"User usr_0M3xL9kQ7vR2nP5wY1jZ4c created order ord_1a2B3c4D5e6F7g..."
|
|
73
|
+
|
|
74
|
+
# vs the nightmare:
|
|
75
|
+
"User 550e8400-e29b-41d4... created order 7c9e6679-7425-40de..."
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Type-safe** - Your type checker catches `user_id = order_id` mistakes before they hit production.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
UserId = UPLID[Literal["usr"]]
|
|
82
|
+
OrgId = UPLID[Literal["org"]]
|
|
83
|
+
|
|
84
|
+
def get_user(user_id: UserId) -> User: ...
|
|
85
|
+
|
|
86
|
+
get_user(org_id) # Type error! Caught by mypy/pyright/ty
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Time-sortable** - Built on UUIDv7. Sort by ID = sort by creation time. No extra column needed.
|
|
90
|
+
|
|
91
|
+
**URL-safe** - 26 characters, no special characters, no encoding. `usr_0M3xL9kQ7vR2nP5wY1jZ4c`
|
|
92
|
+
|
|
93
|
+
**Minimal dependencies** - Just Pydantic. UUID generation uses Python 3.14's stdlib `uuid7()`.
|
|
94
|
+
|
|
95
|
+
> Inspired by Stripe's prefixed IDs (`sk_live_...`, `cus_...`, `pi_...`) - the same pattern trusted by millions of API calls daily.
|
|
96
|
+
|
|
97
|
+
## Pydantic Integration
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from typing import Literal
|
|
101
|
+
from pydantic import BaseModel, Field
|
|
102
|
+
from uplid import UPLID, factory
|
|
103
|
+
|
|
104
|
+
UserId = UPLID[Literal["usr"]]
|
|
105
|
+
|
|
106
|
+
class User(BaseModel):
|
|
107
|
+
id: UserId = Field(default_factory=factory(UserId))
|
|
108
|
+
name: str
|
|
109
|
+
|
|
110
|
+
user = User(name="Alice")
|
|
111
|
+
user.model_dump() # {"id": "usr_0M3xL9kQ7vR2nP5wY1jZ4c", "name": "Alice"}
|
|
112
|
+
|
|
113
|
+
User(id="org_xxx...", name="Bad") # ValidationError: wrong prefix
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## FastAPI Integration
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from typing import Annotated, Literal
|
|
120
|
+
from fastapi import Depends, FastAPI, HTTPException
|
|
121
|
+
from uplid import UPLID, UPLIDError, parse
|
|
122
|
+
|
|
123
|
+
UserId = UPLID[Literal["usr"]]
|
|
124
|
+
parse_user_id = parse(UserId)
|
|
125
|
+
|
|
126
|
+
def validate_user_id(user_id: str) -> UserId:
|
|
127
|
+
try:
|
|
128
|
+
return parse_user_id(user_id)
|
|
129
|
+
except UPLIDError as e:
|
|
130
|
+
raise HTTPException(422, str(e)) from None
|
|
131
|
+
|
|
132
|
+
@app.get("/users/{user_id}")
|
|
133
|
+
def get_user(user_id: Annotated[UserId, Depends(validate_user_id)]) -> User:
|
|
134
|
+
... # user_id is validated and typed
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## SQLAlchemy Integration
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from uplid import UPLID, factory
|
|
141
|
+
from uplid.sqlalchemy import uplid_column
|
|
142
|
+
|
|
143
|
+
UserId = UPLID[Literal["usr"]]
|
|
144
|
+
|
|
145
|
+
class User(Base):
|
|
146
|
+
__tablename__ = "users"
|
|
147
|
+
id: Mapped[UserId] = uplid_column(UserId, primary_key=True)
|
|
148
|
+
name: Mapped[str]
|
|
149
|
+
|
|
150
|
+
# Stores as TEXT, returns as UPLID objects
|
|
151
|
+
user = session.execute(select(User)).scalar_one()
|
|
152
|
+
user.id.prefix # "usr"
|
|
153
|
+
user.id.datetime # When the ID was created
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## SQLModel Integration
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from uplid import UPLID, factory
|
|
160
|
+
from uplid.sqlalchemy import uplid_field
|
|
161
|
+
|
|
162
|
+
UserId = UPLID[Literal["usr"]]
|
|
163
|
+
|
|
164
|
+
class User(SQLModel, table=True):
|
|
165
|
+
id: UserId = uplid_field(UserId, default_factory=factory(UserId), primary_key=True)
|
|
166
|
+
name: str
|
|
167
|
+
|
|
168
|
+
user.model_dump() # {"id": "usr_...", "name": "Alice"} - Pydantic just works
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Prefix Rules
|
|
172
|
+
|
|
173
|
+
Prefixes must be snake_case: lowercase letters and single underscores, cannot start/end with underscore, max 64 characters.
|
|
174
|
+
|
|
175
|
+
Examples: `usr`, `api_key`, `org_member`, `sk_live`
|
|
176
|
+
|
|
177
|
+
## API Reference
|
|
178
|
+
|
|
179
|
+
### `UPLID[PREFIX]`
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
uid = UPLID.generate("usr") # Generate new
|
|
183
|
+
uid = UPLID.from_string("usr_0M3xL9kQ7vR2nP5wY1jZ4c", "usr") # Parse
|
|
184
|
+
|
|
185
|
+
uid.prefix # "usr"
|
|
186
|
+
uid.uid # UUID object
|
|
187
|
+
uid.base62_uid # "0M3xL9kQ7vR2nP5wY1jZ4c"
|
|
188
|
+
uid.datetime # When created (from UUIDv7)
|
|
189
|
+
uid.timestamp # Unix timestamp
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `factory(UPLIDType)` / `parse(UPLIDType)`
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
UserId = UPLID[Literal["usr"]]
|
|
196
|
+
UserIdFactory = factory(UserId) # For Pydantic default_factory
|
|
197
|
+
parse_user_id = parse(UserId) # For manual parsing, raises UPLIDError
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### `UPLIDType`
|
|
201
|
+
|
|
202
|
+
Protocol for functions accepting any UPLID:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
def log_entity(id: UPLIDType) -> None:
|
|
206
|
+
print(f"{id.prefix} created at {id.datetime}")
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### `uplid_column` / `uplid_field`
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from uplid.sqlalchemy import uplid_column, uplid_field
|
|
213
|
+
|
|
214
|
+
# SQLAlchemy
|
|
215
|
+
id: Mapped[UserId] = uplid_column(UserId, primary_key=True)
|
|
216
|
+
|
|
217
|
+
# SQLModel
|
|
218
|
+
id: UserId = uplid_field(UserId, default_factory=factory(UserId), primary_key=True)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
uplid/__init__.py,sha256=gtEVlE30N9TrHaTEpinM5t428HT6NzGjMTDq6ABsKMY,281
|
|
2
|
+
uplid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
uplid/sqlalchemy.py,sha256=FFELyg-7GCUPPiSBncIXSHEkoAaxxoqK1IYvS8Ja5Bg,6311
|
|
4
|
+
uplid/uplid.py,sha256=Gj2sDzOG4vC3HlXoHZzHdhg2ht5stpzE1E15HLS54sg,15844
|
|
5
|
+
uplid-1.1.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
6
|
+
uplid-1.1.0.dist-info/METADATA,sha256=wddvXPWjuXsMr_wX9UDXaB1M8_hqMdkS-HxUGQ5-clA,6241
|
|
7
|
+
uplid-1.1.0.dist-info/RECORD,,
|
uplid-1.0.0.dist-info/METADATA
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: uplid
|
|
3
|
-
Version: 1.0.0
|
|
4
|
-
Summary: Universal Prefixed Literal IDs - type-safe, human-readable identifiers
|
|
5
|
-
Keywords: uuid,id,identifier,pydantic,type-safe,uuid7
|
|
6
|
-
Author: ZVS
|
|
7
|
-
Author-email: ZVS <zvs@daswolf.dev>
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
-
Classifier: Framework :: Pydantic :: 2
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
-
Classifier: Typing :: Typed
|
|
16
|
-
Requires-Dist: pydantic>=2.10
|
|
17
|
-
Requires-Python: >=3.14
|
|
18
|
-
Project-URL: Homepage, https://github.com/zvsdev/uplid
|
|
19
|
-
Project-URL: Repository, https://github.com/zvsdev/uplid
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
|
|
22
|
-
# UPLID
|
|
23
|
-
|
|
24
|
-
Universal Prefixed Literal IDs - type-safe, human-readable identifiers for Python 3.14+.
|
|
25
|
-
|
|
26
|
-
[](https://github.com/zvsdev/uplid/actions/workflows/ci.yml)
|
|
27
|
-
[](https://pypi.org/project/uplid/)
|
|
28
|
-
[](https://pypi.org/project/uplid/)
|
|
29
|
-
|
|
30
|
-
## Features
|
|
31
|
-
|
|
32
|
-
- **Type-safe prefixes**: `UPLID[Literal["usr"]]` prevents mixing user IDs with org IDs
|
|
33
|
-
- **Human-readable**: `usr_4mJ9k2L8nP3qR7sT1vW5xY` (Stripe-style)
|
|
34
|
-
- **Time-sortable**: Built on UUIDv7 for natural ordering
|
|
35
|
-
- **Compact**: 22-character base62 encoding
|
|
36
|
-
- **Zero external deps**: Uses Python 3.14's stdlib `uuid7()`
|
|
37
|
-
- **Pydantic 2 native**: Full validation and serialization support
|
|
38
|
-
|
|
39
|
-
## Installation
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
pip install uplid
|
|
43
|
-
# or
|
|
44
|
-
uv add uplid
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Requires Python 3.14+.
|
|
48
|
-
|
|
49
|
-
## Quick Start
|
|
50
|
-
|
|
51
|
-
```python
|
|
52
|
-
from typing import Literal
|
|
53
|
-
from pydantic import BaseModel, Field
|
|
54
|
-
from uplid import UPLID, factory
|
|
55
|
-
|
|
56
|
-
# Define typed ID aliases
|
|
57
|
-
UserId = UPLID[Literal["usr"]]
|
|
58
|
-
OrgId = UPLID[Literal["org"]]
|
|
59
|
-
|
|
60
|
-
# Use in Pydantic models
|
|
61
|
-
class User(BaseModel):
|
|
62
|
-
id: UserId = Field(default_factory=factory(UserId))
|
|
63
|
-
org_id: OrgId
|
|
64
|
-
|
|
65
|
-
# Generate IDs
|
|
66
|
-
user_id = UPLID.generate("usr")
|
|
67
|
-
print(user_id) # usr_4mJ9k2L8nP3qR7sT1vW5xY
|
|
68
|
-
|
|
69
|
-
# Parse from string
|
|
70
|
-
parsed = UPLID.from_string("usr_4mJ9k2L8nP3qR7sT1vW5xY", "usr")
|
|
71
|
-
|
|
72
|
-
# Type safety - these are compile-time errors with ty/mypy:
|
|
73
|
-
# user.org_id = user_id # Error: UserId != OrgId
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Prefix Rules
|
|
77
|
-
|
|
78
|
-
Prefixes must be snake_case:
|
|
79
|
-
- Lowercase letters and underscores only
|
|
80
|
-
- Cannot start or end with underscore
|
|
81
|
-
- Examples: `usr`, `api_key`, `org_member`
|
|
82
|
-
|
|
83
|
-
## API Reference
|
|
84
|
-
|
|
85
|
-
### `UPLID[PREFIX]`
|
|
86
|
-
|
|
87
|
-
Generic class for prefixed IDs.
|
|
88
|
-
|
|
89
|
-
```python
|
|
90
|
-
# Generate new ID
|
|
91
|
-
uid = UPLID.generate("usr")
|
|
92
|
-
|
|
93
|
-
# Parse from string
|
|
94
|
-
uid = UPLID.from_string("usr_abc123...", "usr")
|
|
95
|
-
|
|
96
|
-
# Properties
|
|
97
|
-
uid.prefix # "usr"
|
|
98
|
-
uid.uid # UUID object
|
|
99
|
-
uid.datetime # datetime from UUIDv7
|
|
100
|
-
uid.timestamp # float (Unix timestamp)
|
|
101
|
-
uid.base62_uid # "abc123..." (22 chars)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### `factory(UPLIDType)`
|
|
105
|
-
|
|
106
|
-
Creates a factory function for Pydantic's `default_factory`.
|
|
107
|
-
|
|
108
|
-
```python
|
|
109
|
-
UserId = UPLID[Literal["usr"]]
|
|
110
|
-
|
|
111
|
-
class User(BaseModel):
|
|
112
|
-
id: UserId = Field(default_factory=factory(UserId))
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### `parse(UPLIDType)`
|
|
116
|
-
|
|
117
|
-
Creates a parser function that raises `UPLIDError` on invalid input.
|
|
118
|
-
|
|
119
|
-
```python
|
|
120
|
-
from uplid import UPLID, parse, UPLIDError
|
|
121
|
-
|
|
122
|
-
UserId = UPLID[Literal["usr"]]
|
|
123
|
-
parse_user_id = parse(UserId)
|
|
124
|
-
|
|
125
|
-
try:
|
|
126
|
-
uid = parse_user_id("usr_abc123...")
|
|
127
|
-
except UPLIDError as e:
|
|
128
|
-
print(e)
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### `UPLIDType`
|
|
132
|
-
|
|
133
|
-
Protocol for generic functions accepting any UPLID:
|
|
134
|
-
|
|
135
|
-
```python
|
|
136
|
-
from uplid import UPLIDType
|
|
137
|
-
|
|
138
|
-
def log_entity(id: UPLIDType) -> None:
|
|
139
|
-
print(f"{id.prefix} created at {id.datetime}")
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### `UPLIDError`
|
|
143
|
-
|
|
144
|
-
Exception raised for invalid IDs. Subclasses `ValueError`.
|
|
145
|
-
|
|
146
|
-
```python
|
|
147
|
-
from uplid import UPLIDError
|
|
148
|
-
|
|
149
|
-
try:
|
|
150
|
-
UPLID.from_string("invalid", "usr")
|
|
151
|
-
except UPLIDError as e:
|
|
152
|
-
print(e)
|
|
153
|
-
except ValueError: # Also works
|
|
154
|
-
pass
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## License
|
|
158
|
-
|
|
159
|
-
MIT
|
uplid-1.0.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
uplid/__init__.py,sha256=96PZKnozPOjHfFbWko0jGzynDNcrr6Sm6jPhwAGnQxU,253
|
|
2
|
-
uplid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
uplid/uplid.py,sha256=krCFV7eFOcFn7KV8R1ko6B9lK37AzcrEJlciMmNvgmA,15108
|
|
4
|
-
uplid-1.0.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
5
|
-
uplid-1.0.0.dist-info/METADATA,sha256=tv2T0oAeG-TuGCd6Ul8E_716ywikTDJoPKkP5IK-muc,3840
|
|
6
|
-
uplid-1.0.0.dist-info/RECORD,,
|
|
File without changes
|