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 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.generate(prefix)
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
+ [![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
@@ -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,,
@@ -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
- [![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
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
@@ -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