uplid 1.0.0__py3-none-any.whl → 1.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.
@@ -0,0 +1,296 @@
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
@@ -1,6 +1,6 @@
1
1
  uplid/__init__.py,sha256=96PZKnozPOjHfFbWko0jGzynDNcrr6Sm6jPhwAGnQxU,253
2
2
  uplid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
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,,
4
+ uplid-1.0.1.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
5
+ uplid-1.0.1.dist-info/METADATA,sha256=OBp_290PWT_NA2dGPyD5TwBcT60QA5Gdb2myQhHaOG8,7704
6
+ uplid-1.0.1.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
File without changes