uplid 1.0.1__tar.gz → 1.1.1__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.
uplid-1.1.1/PKG-INFO ADDED
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: uplid
3
+ Version: 1.1.1
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
+ [![CI](https://github.com/zvsdev/uplid/actions/workflows/ci.yml/badge.svg)](https://github.com/zvsdev/uplid/actions/workflows/ci.yml)
44
+ [![PyPI](https://img.shields.io/pypi/v/uplid)](https://pypi.org/project/uplid/)
45
+ [![Python](https://img.shields.io/pypi/pyversions/uplid)](https://pypi.org/project/uplid/)
46
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](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
uplid-1.1.1/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # UPLID
2
+
3
+ ```
4
+ ██╗ ██╗██████╗ ██╗ ██╗██████╗
5
+ ██║ ██║██╔══██╗██║ ██║██╔══██╗
6
+ ██║ ██║██████╔╝██║ ██║██║ ██║
7
+ ██║ ██║██╔═══╝ ██║ ██║██║ ██║
8
+ ╚██████╔╝██║ ███████╗██║██████╔╝
9
+ ╚═════╝ ╚═╝ ╚══════╝╚═╝╚═════╝
10
+ ```
11
+
12
+ **Stripe-style IDs for Python.**
13
+
14
+ ```python
15
+ # Before: WTF is this?
16
+ "550e8400-e29b-41d4-a716-446655440000"
17
+
18
+ # After: It's a user.
19
+ "usr_0M3xL9kQ7vR2nP5wY1jZ4c"
20
+ ```
21
+
22
+ [![CI](https://github.com/zvsdev/uplid/actions/workflows/ci.yml/badge.svg)](https://github.com/zvsdev/uplid/actions/workflows/ci.yml)
23
+ [![PyPI](https://img.shields.io/pypi/v/uplid)](https://pypi.org/project/uplid/)
24
+ [![Python](https://img.shields.io/pypi/pyversions/uplid)](https://pypi.org/project/uplid/)
25
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/zvsdev/uplid)
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install uplid
31
+ ```
32
+
33
+ Requires Python 3.14+ and Pydantic 2.10+.
34
+
35
+ ## Quick Start
36
+
37
+ ```python
38
+ >>> from uplid import UPLID
39
+ >>> UPLID.generate("usr")
40
+ usr_0M3xL9kQ7vR2nP5wY1jZ4c
41
+ >>> UPLID.generate("ord")
42
+ ord_7x9KmNpQrStUvWxYz012Ab
43
+ ```
44
+
45
+ ## Why UPLID?
46
+
47
+ **Debuggable** - See `usr_` in your logs and instantly know it's a user, not an order, not a session, not a mystery.
48
+
49
+ ```
50
+ # Your logs now:
51
+ "User usr_0M3xL9kQ7vR2nP5wY1jZ4c created order ord_1a2B3c4D5e6F7g..."
52
+
53
+ # vs the nightmare:
54
+ "User 550e8400-e29b-41d4... created order 7c9e6679-7425-40de..."
55
+ ```
56
+
57
+ **Type-safe** - Your type checker catches `user_id = order_id` mistakes before they hit production.
58
+
59
+ ```python
60
+ UserId = UPLID[Literal["usr"]]
61
+ OrgId = UPLID[Literal["org"]]
62
+
63
+ def get_user(user_id: UserId) -> User: ...
64
+
65
+ get_user(org_id) # Type error! Caught by mypy/pyright/ty
66
+ ```
67
+
68
+ **Time-sortable** - Built on UUIDv7. Sort by ID = sort by creation time. No extra column needed.
69
+
70
+ **URL-safe** - 26 characters, no special characters, no encoding. `usr_0M3xL9kQ7vR2nP5wY1jZ4c`
71
+
72
+ **Minimal dependencies** - Just Pydantic. UUID generation uses Python 3.14's stdlib `uuid7()`.
73
+
74
+ > Inspired by Stripe's prefixed IDs (`sk_live_...`, `cus_...`, `pi_...`) - the same pattern trusted by millions of API calls daily.
75
+
76
+ ## Pydantic Integration
77
+
78
+ ```python
79
+ from typing import Literal
80
+ from pydantic import BaseModel, Field
81
+ from uplid import UPLID, factory
82
+
83
+ UserId = UPLID[Literal["usr"]]
84
+
85
+ class User(BaseModel):
86
+ id: UserId = Field(default_factory=factory(UserId))
87
+ name: str
88
+
89
+ user = User(name="Alice")
90
+ user.model_dump() # {"id": "usr_0M3xL9kQ7vR2nP5wY1jZ4c", "name": "Alice"}
91
+
92
+ User(id="org_xxx...", name="Bad") # ValidationError: wrong prefix
93
+ ```
94
+
95
+ ## FastAPI Integration
96
+
97
+ ```python
98
+ from typing import Annotated, Literal
99
+ from fastapi import Depends, FastAPI, HTTPException
100
+ from uplid import UPLID, UPLIDError, parse
101
+
102
+ UserId = UPLID[Literal["usr"]]
103
+ parse_user_id = parse(UserId)
104
+
105
+ def validate_user_id(user_id: str) -> UserId:
106
+ try:
107
+ return parse_user_id(user_id)
108
+ except UPLIDError as e:
109
+ raise HTTPException(422, str(e)) from None
110
+
111
+ @app.get("/users/{user_id}")
112
+ def get_user(user_id: Annotated[UserId, Depends(validate_user_id)]) -> User:
113
+ ... # user_id is validated and typed
114
+ ```
115
+
116
+ ## SQLAlchemy Integration
117
+
118
+ ```python
119
+ from uplid import UPLID, factory
120
+ from uplid.sqlalchemy import uplid_column
121
+
122
+ UserId = UPLID[Literal["usr"]]
123
+
124
+ class User(Base):
125
+ __tablename__ = "users"
126
+ id: Mapped[UserId] = uplid_column(UserId, primary_key=True)
127
+ name: Mapped[str]
128
+
129
+ # Stores as TEXT, returns as UPLID objects
130
+ user = session.execute(select(User)).scalar_one()
131
+ user.id.prefix # "usr"
132
+ user.id.datetime # When the ID was created
133
+ ```
134
+
135
+ ## SQLModel Integration
136
+
137
+ ```python
138
+ from uplid import UPLID, factory
139
+ from uplid.sqlalchemy import uplid_field
140
+
141
+ UserId = UPLID[Literal["usr"]]
142
+
143
+ class User(SQLModel, table=True):
144
+ id: UserId = uplid_field(UserId, default_factory=factory(UserId), primary_key=True)
145
+ name: str
146
+
147
+ user.model_dump() # {"id": "usr_...", "name": "Alice"} - Pydantic just works
148
+ ```
149
+
150
+ ## Prefix Rules
151
+
152
+ Prefixes must be snake_case: lowercase letters and single underscores, cannot start/end with underscore, max 64 characters.
153
+
154
+ Examples: `usr`, `api_key`, `org_member`, `sk_live`
155
+
156
+ ## API Reference
157
+
158
+ ### `UPLID[PREFIX]`
159
+
160
+ ```python
161
+ uid = UPLID.generate("usr") # Generate new
162
+ uid = UPLID.from_string("usr_0M3xL9kQ7vR2nP5wY1jZ4c", "usr") # Parse
163
+
164
+ uid.prefix # "usr"
165
+ uid.uid # UUID object
166
+ uid.base62_uid # "0M3xL9kQ7vR2nP5wY1jZ4c"
167
+ uid.datetime # When created (from UUIDv7)
168
+ uid.timestamp # Unix timestamp
169
+ ```
170
+
171
+ ### `factory(UPLIDType)` / `parse(UPLIDType)`
172
+
173
+ ```python
174
+ UserId = UPLID[Literal["usr"]]
175
+ UserIdFactory = factory(UserId) # For Pydantic default_factory
176
+ parse_user_id = parse(UserId) # For manual parsing, raises UPLIDError
177
+ ```
178
+
179
+ ### `UPLIDType`
180
+
181
+ Protocol for functions accepting any UPLID:
182
+
183
+ ```python
184
+ def log_entity(id: UPLIDType) -> None:
185
+ print(f"{id.prefix} created at {id.datetime}")
186
+ ```
187
+
188
+ ### `uplid_column` / `uplid_field`
189
+
190
+ ```python
191
+ from uplid.sqlalchemy import uplid_column, uplid_field
192
+
193
+ # SQLAlchemy
194
+ id: Mapped[UserId] = uplid_column(UserId, primary_key=True)
195
+
196
+ # SQLModel
197
+ id: UserId = uplid_field(UserId, default_factory=factory(UserId), primary_key=True)
198
+ ```
199
+
200
+ ## License
201
+
202
+ MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uplid"
3
- version = "1.0.1"
3
+ version = "1.1.1"
4
4
  description = "Universal Prefixed Literal IDs - type-safe, human-readable identifiers"
5
5
  authors = [{ name = "ZVS", email = "zvs@daswolf.dev" }]
6
6
  readme = "README.md"
@@ -36,6 +36,7 @@ dev = [
36
36
  "fastapi>=0.115,<1",
37
37
  "httpx>=0.28,<1",
38
38
  "sqlalchemy>=2.0,<3",
39
+ "sqlmodel>=0.0.22,<1",
39
40
  ]
40
41
 
41
42
  [tool.ruff]
@@ -72,7 +73,7 @@ python-version = "3.14"
72
73
 
73
74
  [tool.pytest.ini_options]
74
75
  testpaths = ["tests"]
75
- addopts = ["--cov=uplid", "--cov-report=term-missing", "--cov-fail-under=95"]
76
+ addopts = ["--cov=uplid", "--cov-report=term-missing", "--cov-fail-under=100"]
76
77
 
77
78
  [tool.coverage.run]
78
79
  branch = true
@@ -0,0 +1,8 @@
1
+ """Universal Prefixed Literal IDs - type-safe, human-readable identifiers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uplid.uplid import UPLID, UPLIDError, UPLIDType, _get_prefix, factory, parse
6
+
7
+
8
+ __all__ = ["UPLID", "UPLIDError", "UPLIDType", "_get_prefix", "factory", "parse"]
@@ -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"]
@@ -7,7 +7,6 @@ from datetime import UTC
7
7
  from datetime import datetime as dt_datetime
8
8
  from typing import (
9
9
  TYPE_CHECKING,
10
- Any,
11
10
  LiteralString,
12
11
  Protocol,
13
12
  Self,
@@ -234,24 +233,28 @@ class UPLID[PREFIX: LiteralString]:
234
233
  def __lt__(self, other: object) -> bool:
235
234
  """Compare for sorting (by prefix, then by uid)."""
236
235
  if isinstance(other, UPLID):
236
+ # type: ignore needed because UUID comparison is not recognized by type checkers
237
237
  return (self._prefix, self._uid) < (other._prefix, other._uid) # type: ignore[operator]
238
238
  return NotImplemented
239
239
 
240
240
  def __le__(self, other: object) -> bool:
241
241
  """Compare for sorting (by prefix, then by uid)."""
242
242
  if isinstance(other, UPLID):
243
+ # type: ignore needed because UUID comparison is not recognized by type checkers
243
244
  return (self._prefix, self._uid) <= (other._prefix, other._uid) # type: ignore[operator]
244
245
  return NotImplemented
245
246
 
246
247
  def __gt__(self, other: object) -> bool:
247
248
  """Compare for sorting (by prefix, then by uid)."""
248
249
  if isinstance(other, UPLID):
250
+ # type: ignore needed because UUID comparison is not recognized by type checkers
249
251
  return (self._prefix, self._uid) > (other._prefix, other._uid) # type: ignore[operator]
250
252
  return NotImplemented
251
253
 
252
254
  def __ge__(self, other: object) -> bool:
253
255
  """Compare for sorting (by prefix, then by uid)."""
254
256
  if isinstance(other, UPLID):
257
+ # type: ignore needed because UUID comparison is not recognized by type checkers
255
258
  return (self._prefix, self._uid) >= (other._prefix, other._uid) # type: ignore[operator]
256
259
  return NotImplemented
257
260
 
@@ -259,7 +262,7 @@ class UPLID[PREFIX: LiteralString]:
259
262
  """Return self (UPLIDs are immutable)."""
260
263
  return self
261
264
 
262
- def __deepcopy__(self, memo: dict[int, Any]) -> Self:
265
+ def __deepcopy__(self, memo: object) -> Self:
263
266
  """Return self (UPLIDs are immutable)."""
264
267
  return self
265
268
 
@@ -282,6 +285,14 @@ class UPLID[PREFIX: LiteralString]:
282
285
  UPLIDError: If the prefix is not valid snake_case.
283
286
  """
284
287
  _validate_prefix(prefix)
288
+ return cls._generate_unchecked(prefix)
289
+
290
+ @classmethod
291
+ def _generate_unchecked(cls, prefix: PREFIX) -> Self:
292
+ """Generate a new UPLID without validating the prefix.
293
+
294
+ Internal method for use when prefix has already been validated.
295
+ """
285
296
  instance = cls.__new__(cls)
286
297
  instance._prefix = prefix # noqa: SLF001
287
298
  instance._uid = uuid7() # noqa: SLF001
@@ -339,8 +350,8 @@ class UPLID[PREFIX: LiteralString]:
339
350
  @classmethod
340
351
  def __get_pydantic_core_schema__(
341
352
  cls,
342
- source_type: Any, # noqa: ANN401
343
- handler: Any, # noqa: ANN401
353
+ source_type: type[Self],
354
+ handler: object,
344
355
  ) -> CoreSchema:
345
356
  """Pydantic integration for validation and serialization.
346
357
 
@@ -371,7 +382,7 @@ class UPLID[PREFIX: LiteralString]:
371
382
 
372
383
  prefix_str: str = prefix_args[0]
373
384
 
374
- def validate(v: UPLID[Any] | str) -> UPLID[Any]:
385
+ def validate(v: UPLIDType | str) -> UPLIDType:
375
386
  if isinstance(v, str):
376
387
  return cls.from_string(v, prefix_str)
377
388
  if isinstance(v, UPLID):
@@ -421,9 +432,10 @@ def factory[PREFIX: LiteralString](
421
432
  id: UserId = Field(default_factory=factory(UserId))
422
433
  """
423
434
  prefix = _get_prefix(uplid_type)
435
+ _validate_prefix(prefix) # Validate once at factory creation
424
436
 
425
437
  def _factory() -> UPLID[PREFIX]:
426
- return UPLID.generate(prefix)
438
+ return UPLID._generate_unchecked(prefix) # noqa: SLF001
427
439
 
428
440
  return _factory
429
441
 
uplid-1.0.1/PKG-INFO DELETED
@@ -1,296 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: uplid
3
- Version: 1.0.1
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
- [![CI](https://github.com/zvsdev/uplid/actions/workflows/ci.yml/badge.svg)](https://github.com/zvsdev/uplid/actions/workflows/ci.yml)
27
- [![PyPI](https://img.shields.io/pypi/v/uplid)](https://pypi.org/project/uplid/)
28
- [![Python](https://img.shields.io/pypi/pyversions/uplid)](https://pypi.org/project/uplid/)
29
-
30
- ## Features
31
-
32
- - **Type-safe prefixes**: `UPLID[Literal["usr"]]` prevents mixing user IDs with org IDs at compile time
33
- - **Human-readable**: `usr_0M3xL9kQ7vR2nP5wY1jZ4c` (Stripe-style prefixed IDs)
34
- - **Time-sortable**: Built on UUIDv7 (RFC 9562) for natural chronological ordering
35
- - **Compact**: 22-character base62 encoding (URL-safe, no special characters)
36
- - **Stdlib UUIDs**: Uses Python 3.14's native `uuid7()` - no external UUID libraries
37
- - **Pydantic 2 native**: Full validation and serialization support
38
- - **Thread-safe**: ID generation is safe for concurrent use
39
-
40
- ## Installation
41
-
42
- ```bash
43
- pip install uplid
44
- # or
45
- uv add uplid
46
- ```
47
-
48
- Requires Python 3.14+ and Pydantic 2.10+.
49
-
50
- ## Quick Start
51
-
52
- ```python
53
- from typing import Literal
54
- from pydantic import BaseModel, Field
55
- from uplid import UPLID, factory
56
-
57
- # Define typed aliases and factories
58
- UserId = UPLID[Literal["usr"]]
59
- OrgId = UPLID[Literal["org"]]
60
- UserIdFactory = factory(UserId)
61
-
62
- # Use in Pydantic models
63
- class User(BaseModel):
64
- id: UserId = Field(default_factory=UserIdFactory)
65
- org_id: OrgId
66
-
67
- # Generate IDs
68
- user_id = UPLID.generate("usr")
69
- print(user_id) # usr_0M3xL9kQ7vR2nP5wY1jZ4c
70
-
71
- # Parse from string
72
- parsed = UPLID.from_string("usr_0M3xL9kQ7vR2nP5wY1jZ4c", "usr")
73
-
74
- # Access properties
75
- print(parsed.datetime) # 2026-01-30 12:34:56.789000+00:00
76
- print(parsed.timestamp) # 1738240496.789
77
-
78
- # Type safety - these are compile-time errors:
79
- # user.org_id = user_id # Error: UserId is not compatible with OrgId
80
- ```
81
-
82
- ## Pydantic Serialization
83
-
84
- UPLIDs serialize to strings and deserialize with validation:
85
-
86
- ```python
87
- from pydantic import BaseModel, Field
88
- from uplid import UPLID, factory
89
-
90
- UserId = UPLID[Literal["usr"]]
91
- UserIdFactory = factory(UserId)
92
-
93
- class User(BaseModel):
94
- id: UserId = Field(default_factory=UserIdFactory)
95
- name: str
96
-
97
- user = User(name="Alice")
98
-
99
- # Serialize to dict - ID becomes string
100
- user.model_dump()
101
- # {"id": "usr_0M3xL9kQ7vR2nP5wY1jZ4c", "name": "Alice"}
102
-
103
- # Serialize to JSON
104
- json_str = user.model_dump_json()
105
-
106
- # Deserialize - validates UPLID format and prefix
107
- restored = User.model_validate_json(json_str)
108
- assert restored.id == user.id
109
-
110
- # Wrong prefix raises ValidationError
111
- User(id="org_xxx...", name="Bad") # ValidationError
112
- ```
113
-
114
- ## FastAPI Integration
115
-
116
- ```python
117
- from typing import Annotated, Literal
118
- from fastapi import Cookie, Depends, FastAPI, Header, HTTPException
119
- from pydantic import BaseModel, Field
120
- from uplid import UPLID, UPLIDError, factory, parse
121
-
122
- UserId = UPLID[Literal["usr"]]
123
- UserIdFactory = factory(UserId)
124
- parse_user_id = parse(UserId)
125
-
126
-
127
- class User(BaseModel):
128
- id: UserId = Field(default_factory=UserIdFactory)
129
- name: str
130
-
131
-
132
- app = FastAPI()
133
-
134
-
135
- # Dependency for validating path/query/header/cookie parameters
136
- def get_user_id(user_id: str) -> UserId:
137
- try:
138
- return parse_user_id(user_id)
139
- except UPLIDError as e:
140
- raise HTTPException(422, f"Invalid user ID: {e}") from None
141
-
142
-
143
- # Path parameter validation
144
- @app.get("/users/{user_id}")
145
- def get_user(user_id: Annotated[UserId, Depends(get_user_id)]) -> User:
146
- ...
147
-
148
-
149
- # JSON body - Pydantic validates UPLID fields automatically
150
- @app.post("/users")
151
- def create_user(user: User) -> User:
152
- # user.id validated as UserId, wrong prefix returns 422
153
- return user
154
-
155
-
156
- # Header validation
157
- def get_user_id_from_header(x_user_id: Annotated[str, Header()]) -> UserId:
158
- try:
159
- return parse_user_id(x_user_id)
160
- except UPLIDError as e:
161
- raise HTTPException(422, f"Invalid X-User-Id header: {e}") from None
162
-
163
-
164
- @app.get("/me")
165
- def get_current_user(user_id: Annotated[UserId, Depends(get_user_id_from_header)]) -> User:
166
- ...
167
-
168
-
169
- # Cookie validation
170
- def get_session_user(session_user_id: Annotated[str, Cookie()]) -> UserId:
171
- try:
172
- return parse_user_id(session_user_id)
173
- except UPLIDError as e:
174
- raise HTTPException(422, f"Invalid session cookie: {e}") from None
175
-
176
-
177
- @app.get("/session")
178
- def get_session(user_id: Annotated[UserId, Depends(get_session_user)]) -> User:
179
- ...
180
- ```
181
-
182
- ## Database Storage
183
-
184
- UPLIDs serialize to strings. Store as `VARCHAR(87)` (64 char prefix + 1 underscore + 22 char base62):
185
-
186
- ```python
187
- from typing import Literal
188
- from sqlalchemy import String, create_engine
189
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
190
- from uplid import UPLID, factory
191
-
192
- UserId = UPLID[Literal["usr"]]
193
- UserIdFactory = factory(UserId)
194
-
195
-
196
- class Base(DeclarativeBase):
197
- pass
198
-
199
-
200
- class UserRow(Base):
201
- __tablename__ = "users"
202
-
203
- id: Mapped[str] = mapped_column(String(87), primary_key=True)
204
- name: Mapped[str] = mapped_column(String(100))
205
-
206
-
207
- # Create with UPLID, store as string
208
- engine = create_engine("sqlite:///:memory:")
209
- Base.metadata.create_all(engine)
210
-
211
- with Session(engine) as session:
212
- user = UserRow(id=str(UPLID.generate("usr")), name="Alice")
213
- session.add(user)
214
- session.commit()
215
-
216
- # Query and parse back to UPLID
217
- row = session.query(UserRow).first()
218
- user_id = UPLID.from_string(row.id, "usr")
219
- print(user_id.datetime) # When the ID was created
220
- ```
221
-
222
- ## Prefix Rules
223
-
224
- Prefixes must be snake_case:
225
- - Lowercase letters and single underscores only
226
- - Cannot start or end with underscore
227
- - Maximum 64 characters
228
- - Examples: `usr`, `api_key`, `org_member`
229
-
230
- ## API Reference
231
-
232
- ### `UPLID[PREFIX]`
233
-
234
- Generic class for prefixed IDs.
235
-
236
- ```python
237
- # Generate new ID
238
- uid = UPLID.generate("usr")
239
-
240
- # Parse from string
241
- uid = UPLID.from_string("usr_0M3xL9kQ7vR2nP5wY1jZ4c", "usr")
242
-
243
- # Properties
244
- uid.prefix # str: "usr"
245
- uid.uid # UUID: underlying UUIDv7
246
- uid.base62_uid # str: 22-char base62 encoding
247
- uid.datetime # datetime: UTC timestamp from UUIDv7
248
- uid.timestamp # float: Unix timestamp in seconds
249
- ```
250
-
251
- ### `factory(UPLIDType)`
252
-
253
- Creates a factory function for Pydantic's `default_factory`.
254
-
255
- ```python
256
- UserId = UPLID[Literal["usr"]]
257
- UserIdFactory = factory(UserId)
258
-
259
- class User(BaseModel):
260
- id: UserId = Field(default_factory=UserIdFactory)
261
- ```
262
-
263
- ### `parse(UPLIDType)`
264
-
265
- Creates a parser function that raises `UPLIDError` on invalid input.
266
-
267
- ```python
268
- from uplid import UPLID, parse, UPLIDError
269
-
270
- UserId = UPLID[Literal["usr"]]
271
- parse_user_id = parse(UserId)
272
-
273
- try:
274
- uid = parse_user_id("usr_0M3xL9kQ7vR2nP5wY1jZ4c")
275
- except UPLIDError as e:
276
- print(e)
277
- ```
278
-
279
- ### `UPLIDType`
280
-
281
- Protocol for generic functions accepting any UPLID:
282
-
283
- ```python
284
- from uplid import UPLIDType
285
-
286
- def log_entity(id: UPLIDType) -> None:
287
- print(f"{id.prefix} created at {id.datetime}")
288
- ```
289
-
290
- ### `UPLIDError`
291
-
292
- Exception raised for invalid IDs. Subclasses `ValueError`.
293
-
294
- ## License
295
-
296
- MIT
uplid-1.0.1/README.md DELETED
@@ -1,275 +0,0 @@
1
- # UPLID
2
-
3
- Universal Prefixed Literal IDs - type-safe, human-readable identifiers for Python 3.14+.
4
-
5
- [![CI](https://github.com/zvsdev/uplid/actions/workflows/ci.yml/badge.svg)](https://github.com/zvsdev/uplid/actions/workflows/ci.yml)
6
- [![PyPI](https://img.shields.io/pypi/v/uplid)](https://pypi.org/project/uplid/)
7
- [![Python](https://img.shields.io/pypi/pyversions/uplid)](https://pypi.org/project/uplid/)
8
-
9
- ## Features
10
-
11
- - **Type-safe prefixes**: `UPLID[Literal["usr"]]` prevents mixing user IDs with org IDs at compile time
12
- - **Human-readable**: `usr_0M3xL9kQ7vR2nP5wY1jZ4c` (Stripe-style prefixed IDs)
13
- - **Time-sortable**: Built on UUIDv7 (RFC 9562) for natural chronological ordering
14
- - **Compact**: 22-character base62 encoding (URL-safe, no special characters)
15
- - **Stdlib UUIDs**: Uses Python 3.14's native `uuid7()` - no external UUID libraries
16
- - **Pydantic 2 native**: Full validation and serialization support
17
- - **Thread-safe**: ID generation is safe for concurrent use
18
-
19
- ## Installation
20
-
21
- ```bash
22
- pip install uplid
23
- # or
24
- uv add uplid
25
- ```
26
-
27
- Requires Python 3.14+ and Pydantic 2.10+.
28
-
29
- ## Quick Start
30
-
31
- ```python
32
- from typing import Literal
33
- from pydantic import BaseModel, Field
34
- from uplid import UPLID, factory
35
-
36
- # Define typed aliases and factories
37
- UserId = UPLID[Literal["usr"]]
38
- OrgId = UPLID[Literal["org"]]
39
- UserIdFactory = factory(UserId)
40
-
41
- # Use in Pydantic models
42
- class User(BaseModel):
43
- id: UserId = Field(default_factory=UserIdFactory)
44
- org_id: OrgId
45
-
46
- # Generate IDs
47
- user_id = UPLID.generate("usr")
48
- print(user_id) # usr_0M3xL9kQ7vR2nP5wY1jZ4c
49
-
50
- # Parse from string
51
- parsed = UPLID.from_string("usr_0M3xL9kQ7vR2nP5wY1jZ4c", "usr")
52
-
53
- # Access properties
54
- print(parsed.datetime) # 2026-01-30 12:34:56.789000+00:00
55
- print(parsed.timestamp) # 1738240496.789
56
-
57
- # Type safety - these are compile-time errors:
58
- # user.org_id = user_id # Error: UserId is not compatible with OrgId
59
- ```
60
-
61
- ## Pydantic Serialization
62
-
63
- UPLIDs serialize to strings and deserialize with validation:
64
-
65
- ```python
66
- from pydantic import BaseModel, Field
67
- from uplid import UPLID, factory
68
-
69
- UserId = UPLID[Literal["usr"]]
70
- UserIdFactory = factory(UserId)
71
-
72
- class User(BaseModel):
73
- id: UserId = Field(default_factory=UserIdFactory)
74
- name: str
75
-
76
- user = User(name="Alice")
77
-
78
- # Serialize to dict - ID becomes string
79
- user.model_dump()
80
- # {"id": "usr_0M3xL9kQ7vR2nP5wY1jZ4c", "name": "Alice"}
81
-
82
- # Serialize to JSON
83
- json_str = user.model_dump_json()
84
-
85
- # Deserialize - validates UPLID format and prefix
86
- restored = User.model_validate_json(json_str)
87
- assert restored.id == user.id
88
-
89
- # Wrong prefix raises ValidationError
90
- User(id="org_xxx...", name="Bad") # ValidationError
91
- ```
92
-
93
- ## FastAPI Integration
94
-
95
- ```python
96
- from typing import Annotated, Literal
97
- from fastapi import Cookie, Depends, FastAPI, Header, HTTPException
98
- from pydantic import BaseModel, Field
99
- from uplid import UPLID, UPLIDError, factory, parse
100
-
101
- UserId = UPLID[Literal["usr"]]
102
- UserIdFactory = factory(UserId)
103
- parse_user_id = parse(UserId)
104
-
105
-
106
- class User(BaseModel):
107
- id: UserId = Field(default_factory=UserIdFactory)
108
- name: str
109
-
110
-
111
- app = FastAPI()
112
-
113
-
114
- # Dependency for validating path/query/header/cookie parameters
115
- def get_user_id(user_id: str) -> UserId:
116
- try:
117
- return parse_user_id(user_id)
118
- except UPLIDError as e:
119
- raise HTTPException(422, f"Invalid user ID: {e}") from None
120
-
121
-
122
- # Path parameter validation
123
- @app.get("/users/{user_id}")
124
- def get_user(user_id: Annotated[UserId, Depends(get_user_id)]) -> User:
125
- ...
126
-
127
-
128
- # JSON body - Pydantic validates UPLID fields automatically
129
- @app.post("/users")
130
- def create_user(user: User) -> User:
131
- # user.id validated as UserId, wrong prefix returns 422
132
- return user
133
-
134
-
135
- # Header validation
136
- def get_user_id_from_header(x_user_id: Annotated[str, Header()]) -> UserId:
137
- try:
138
- return parse_user_id(x_user_id)
139
- except UPLIDError as e:
140
- raise HTTPException(422, f"Invalid X-User-Id header: {e}") from None
141
-
142
-
143
- @app.get("/me")
144
- def get_current_user(user_id: Annotated[UserId, Depends(get_user_id_from_header)]) -> User:
145
- ...
146
-
147
-
148
- # Cookie validation
149
- def get_session_user(session_user_id: Annotated[str, Cookie()]) -> UserId:
150
- try:
151
- return parse_user_id(session_user_id)
152
- except UPLIDError as e:
153
- raise HTTPException(422, f"Invalid session cookie: {e}") from None
154
-
155
-
156
- @app.get("/session")
157
- def get_session(user_id: Annotated[UserId, Depends(get_session_user)]) -> User:
158
- ...
159
- ```
160
-
161
- ## Database Storage
162
-
163
- UPLIDs serialize to strings. Store as `VARCHAR(87)` (64 char prefix + 1 underscore + 22 char base62):
164
-
165
- ```python
166
- from typing import Literal
167
- from sqlalchemy import String, create_engine
168
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
169
- from uplid import UPLID, factory
170
-
171
- UserId = UPLID[Literal["usr"]]
172
- UserIdFactory = factory(UserId)
173
-
174
-
175
- class Base(DeclarativeBase):
176
- pass
177
-
178
-
179
- class UserRow(Base):
180
- __tablename__ = "users"
181
-
182
- id: Mapped[str] = mapped_column(String(87), primary_key=True)
183
- name: Mapped[str] = mapped_column(String(100))
184
-
185
-
186
- # Create with UPLID, store as string
187
- engine = create_engine("sqlite:///:memory:")
188
- Base.metadata.create_all(engine)
189
-
190
- with Session(engine) as session:
191
- user = UserRow(id=str(UPLID.generate("usr")), name="Alice")
192
- session.add(user)
193
- session.commit()
194
-
195
- # Query and parse back to UPLID
196
- row = session.query(UserRow).first()
197
- user_id = UPLID.from_string(row.id, "usr")
198
- print(user_id.datetime) # When the ID was created
199
- ```
200
-
201
- ## Prefix Rules
202
-
203
- Prefixes must be snake_case:
204
- - Lowercase letters and single underscores only
205
- - Cannot start or end with underscore
206
- - Maximum 64 characters
207
- - Examples: `usr`, `api_key`, `org_member`
208
-
209
- ## API Reference
210
-
211
- ### `UPLID[PREFIX]`
212
-
213
- Generic class for prefixed IDs.
214
-
215
- ```python
216
- # Generate new ID
217
- uid = UPLID.generate("usr")
218
-
219
- # Parse from string
220
- uid = UPLID.from_string("usr_0M3xL9kQ7vR2nP5wY1jZ4c", "usr")
221
-
222
- # Properties
223
- uid.prefix # str: "usr"
224
- uid.uid # UUID: underlying UUIDv7
225
- uid.base62_uid # str: 22-char base62 encoding
226
- uid.datetime # datetime: UTC timestamp from UUIDv7
227
- uid.timestamp # float: Unix timestamp in seconds
228
- ```
229
-
230
- ### `factory(UPLIDType)`
231
-
232
- Creates a factory function for Pydantic's `default_factory`.
233
-
234
- ```python
235
- UserId = UPLID[Literal["usr"]]
236
- UserIdFactory = factory(UserId)
237
-
238
- class User(BaseModel):
239
- id: UserId = Field(default_factory=UserIdFactory)
240
- ```
241
-
242
- ### `parse(UPLIDType)`
243
-
244
- Creates a parser function that raises `UPLIDError` on invalid input.
245
-
246
- ```python
247
- from uplid import UPLID, parse, UPLIDError
248
-
249
- UserId = UPLID[Literal["usr"]]
250
- parse_user_id = parse(UserId)
251
-
252
- try:
253
- uid = parse_user_id("usr_0M3xL9kQ7vR2nP5wY1jZ4c")
254
- except UPLIDError as e:
255
- print(e)
256
- ```
257
-
258
- ### `UPLIDType`
259
-
260
- Protocol for generic functions accepting any UPLID:
261
-
262
- ```python
263
- from uplid import UPLIDType
264
-
265
- def log_entity(id: UPLIDType) -> None:
266
- print(f"{id.prefix} created at {id.datetime}")
267
- ```
268
-
269
- ### `UPLIDError`
270
-
271
- Exception raised for invalid IDs. Subclasses `ValueError`.
272
-
273
- ## License
274
-
275
- MIT
@@ -1,8 +0,0 @@
1
- """Universal Prefixed Literal IDs - type-safe, human-readable identifiers."""
2
-
3
- from __future__ import annotations
4
-
5
- from uplid.uplid import UPLID, UPLIDError, UPLIDType, factory, parse
6
-
7
-
8
- __all__ = ["UPLID", "UPLIDError", "UPLIDType", "factory", "parse"]
File without changes