base-typed-string 0.1.0__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.
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eldeniz Guseinli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,536 @@
1
+ Metadata-Version: 2.4
2
+ Name: base-typed-string
3
+ Version: 0.1.0
4
+ Summary: Strict typed string base class with exact runtime subtype preservation and optional Pydantic v2 support.
5
+ Author-email: Eldeniz Guseinli <eldenizfamilyanskicode@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/eldenizfamilyanskicode/base-typed-string
8
+ Project-URL: Repository, https://github.com/eldenizfamilyanskicode/base-typed-string
9
+ Project-URL: Issues, https://github.com/eldenizfamilyanskicode/base-typed-string/issues
10
+ Keywords: typing,typed-string,value-object,pydantic,domain-model
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: Implementation :: CPython
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: pydantic
25
+ Requires-Dist: pydantic<3,>=2.6; extra == "pydantic"
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=8.0; extra == "test"
28
+ Requires-Dist: pytest-cov>=5.0; extra == "test"
29
+ Provides-Extra: lint
30
+ Requires-Dist: ruff>=0.5; extra == "lint"
31
+ Provides-Extra: typecheck
32
+ Requires-Dist: mypy>=1.10; extra == "typecheck"
33
+ Requires-Dist: pyright>=1.1; extra == "typecheck"
34
+ Provides-Extra: build
35
+ Requires-Dist: build>=1.2; extra == "build"
36
+ Requires-Dist: twine>=5.1; extra == "build"
37
+ Provides-Extra: dev
38
+ Requires-Dist: build>=1.2; extra == "dev"
39
+ Requires-Dist: twine>=5.1; extra == "dev"
40
+ Requires-Dist: mypy>=1.10; extra == "dev"
41
+ Requires-Dist: pyright>=1.1; extra == "dev"
42
+ Requires-Dist: pytest>=8.0; extra == "dev"
43
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
44
+ Requires-Dist: ruff>=0.5; extra == "dev"
45
+ Requires-Dist: pydantic<3,>=2.6; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # base-typed-string
49
+
50
+ Strict typed string base class with exact runtime subtype preservation.
51
+
52
+ `base_typed_string` is a small Python library for building domain-specific string types that remain real `str` objects at runtime.
53
+
54
+ It is designed for codebases where values such as `UserName`, `EmailAddress`, `AccountKey`, or `RawInputStr` should be:
55
+
56
+ - strongly named in type annotations
57
+ - real `str` objects at runtime
58
+ - serializable as plain strings
59
+ - reconstructable at validation boundaries
60
+ - lightweight and predictable
61
+
62
+ ---
63
+
64
+ ## Why
65
+
66
+ Sometimes a value is semantically important enough to deserve its own type, but operationally it should still behave like a normal Python string.
67
+
68
+ Examples:
69
+
70
+ - `UserName`
71
+ - `EmailAddress`
72
+ - `AccountKey`
73
+ - `RawInputStr`
74
+ - `IntegrationName`
75
+ - `ValidatedInputStr`
76
+
77
+ Using plain `str` everywhere loses domain meaning.
78
+ Using wrappers changes runtime behavior.
79
+ Using `NewType` helps only static typing.
80
+
81
+ `base_typed_string` gives you a middle ground:
82
+ domain-specific names in type annotations, while keeping real `str` behavior at runtime.
83
+
84
+ ---
85
+
86
+ ## What it guarantees
87
+
88
+ - accepts only `str`
89
+ - preserves the exact subclass type at construction time
90
+ - behaves like normal `str`
91
+ - normal string operations return plain `str`
92
+ - preserves subtype through pickle roundtrip
93
+ - supports Pydantic v2, but does not require it
94
+ - ships `py.typed`
95
+
96
+ ---
97
+
98
+ ## What it intentionally does not do
99
+
100
+ - no built-in validation rules
101
+ - no normalization
102
+ - no regex engine
103
+ - no domain-specific methods
104
+ - no custom JSON layer
105
+
106
+ This package is intentionally minimal.
107
+
108
+ Domain rules should live in your subclasses or in your application layer.
109
+
110
+ ---
111
+
112
+ ## Why not plain `str` / `NewType` / custom wrapper?
113
+
114
+ ### Why not plain `str`?
115
+
116
+ Because plain `str` does not communicate domain intent.
117
+
118
+ ```python
119
+ def create_user(user_name: str, email_address: str) -> None:
120
+ ...
121
+ ````
122
+
123
+ This is easy to misuse:
124
+
125
+ * parameters can be swapped accidentally
126
+ * type annotations do not explain domain meaning
127
+ * static analysis cannot distinguish semantic string types
128
+
129
+ With typed subclasses:
130
+
131
+ ```python
132
+ def create_user(user_name: UserName, email_address: EmailAddress) -> None:
133
+ ...
134
+ ```
135
+
136
+ the intent is explicit.
137
+
138
+ ### Why not `typing.NewType`?
139
+
140
+ `NewType` is a static typing tool, not a runtime type.
141
+
142
+ ```python
143
+ from typing import NewType
144
+
145
+ UserName = NewType("UserName", str)
146
+
147
+ user_name: UserName = UserName("alice")
148
+
149
+ assert type(user_name) is str
150
+ assert isinstance(user_name, str)
151
+ ```
152
+
153
+ This means:
154
+
155
+ * runtime values are still plain `str`
156
+ * there is no real subclass at runtime
157
+ * runtime boundaries cannot preserve a concrete semantic subtype
158
+ * introspection and runtime behavior cannot distinguish `UserName` from plain `str`
159
+
160
+ `base_typed_string` creates a real runtime subtype instead.
161
+
162
+ ### Why not a custom wrapper class?
163
+
164
+ A wrapper can model a domain value, but it stops being a real string.
165
+
166
+ Typical trade-offs:
167
+
168
+ * `isinstance(value, str)` becomes `False`
169
+ * JSON serialization often needs custom handling
170
+ * many libraries expect plain `str`, not wrapper objects
171
+ * you often need explicit `.value` extraction
172
+ * interoperability becomes noisier
173
+
174
+ A wrapper is useful when you want rich behavior and strict encapsulation.
175
+
176
+ `base_typed_string` is for the opposite case:
177
+ keep the value operationally identical to `str`, while still having a named domain type.
178
+
179
+ ### When `base_typed_string` is the right choice
180
+
181
+ Use it when you want:
182
+
183
+ * semantic string types in annotations
184
+ * real `str` behavior at runtime
185
+ * plain string serialization
186
+ * clean interoperability with Python and library code
187
+
188
+ Do not use it when you need:
189
+
190
+ * heavy domain logic on the value object
191
+ * mutable state
192
+ * multiple fields
193
+ * non-string runtime representation
194
+
195
+ ---
196
+
197
+ ## Installation
198
+
199
+ ### Base package
200
+
201
+ ```bash
202
+ pip install base-typed-string
203
+ ```
204
+
205
+ ### With Pydantic v2 support
206
+
207
+ ```bash
208
+ pip install "base-typed-string[pydantic]"
209
+ ```
210
+
211
+ If Pydantic v2 is already installed in your project, integration works automatically.
212
+
213
+ ### For development
214
+
215
+ ```bash
216
+ pip install "base-typed-string[dev]"
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Quick start
222
+
223
+ ```python
224
+ from base_typed_string import BaseTypedString
225
+
226
+
227
+ class UserName(BaseTypedString):
228
+ pass
229
+
230
+
231
+ user_name: UserName = UserName("alice")
232
+
233
+ assert user_name == "alice"
234
+ assert isinstance(user_name, str)
235
+ assert isinstance(user_name, UserName)
236
+ assert type(user_name) is UserName
237
+ ```
238
+
239
+ ---
240
+
241
+ ## How to use it in your project
242
+
243
+ Create a module for your domain string types.
244
+
245
+ For example, create a file named `domain_typings.py`:
246
+
247
+ ```python
248
+ from base_typed_string import BaseTypedString
249
+
250
+
251
+ class UserName(BaseTypedString):
252
+ """User login name."""
253
+
254
+
255
+ class EmailAddress(BaseTypedString):
256
+ """User email address."""
257
+ ```
258
+
259
+ Then use these types in your application code:
260
+
261
+ ```python
262
+ from .domain_typings import EmailAddress, UserName
263
+
264
+
265
+ def create_user(user_name: UserName, email_address: EmailAddress) -> None:
266
+ print(user_name, email_address)
267
+ ```
268
+
269
+ This gives you:
270
+
271
+ * domain-specific names in type annotations
272
+ * real `str` values at runtime
273
+ * plain string serialization behavior
274
+ * reconstruction through validation layers such as Pydantic
275
+
276
+ ---
277
+
278
+ ## Runtime behavior
279
+
280
+ `BaseTypedString` is a real `str` subclass.
281
+
282
+ ```python
283
+ from base_typed_string import BaseTypedString
284
+
285
+
286
+ class UserName(BaseTypedString):
287
+ pass
288
+
289
+
290
+ user_name: UserName = UserName("alice")
291
+
292
+ assert isinstance(user_name, str)
293
+ assert isinstance(user_name, UserName)
294
+ assert type(user_name) is UserName
295
+ assert user_name == "alice"
296
+ ```
297
+
298
+ ### Normal string operations return plain `str`
299
+
300
+ ```python
301
+ from base_typed_string import BaseTypedString
302
+
303
+
304
+ class UserName(BaseTypedString):
305
+ pass
306
+
307
+
308
+ user_name: UserName = UserName("alice")
309
+
310
+ uppercased_value: str = user_name.upper()
311
+ concatenated_value: str = user_name + "!"
312
+ replaced_value: str = user_name.replace("a", "A")
313
+
314
+ assert type(uppercased_value) is str
315
+ assert type(concatenated_value) is str
316
+ assert type(replaced_value) is str
317
+ ```
318
+
319
+ This behavior is intentional.
320
+
321
+ The typed subtype is preserved at construction and validation boundaries, not across ordinary string operations.
322
+
323
+ ---
324
+
325
+ ## Constructor rules
326
+
327
+ Only `str` values are accepted.
328
+
329
+ ```python
330
+ from base_typed_string import BaseTypedString
331
+
332
+
333
+ class UserName(BaseTypedString):
334
+ pass
335
+
336
+
337
+ UserName("alice") # valid
338
+ UserName(123) # raises BaseTypedStringInvalidInputValueError
339
+ UserName(None) # raises BaseTypedStringInvalidInputValueError
340
+ ```
341
+
342
+ Existing typed string instances are also accepted because they are still real strings:
343
+
344
+ ```python
345
+ from base_typed_string import BaseTypedString
346
+
347
+
348
+ class UserName(BaseTypedString):
349
+ pass
350
+
351
+
352
+ source_user_name: UserName = UserName("alice")
353
+ copied_user_name: UserName = UserName(source_user_name)
354
+
355
+ assert copied_user_name == "alice"
356
+ assert type(copied_user_name) is UserName
357
+ ```
358
+
359
+ Direct instantiation of the base class is also supported:
360
+
361
+ ```python
362
+ from base_typed_string import BaseTypedString
363
+
364
+
365
+ plain_typed_value: BaseTypedString = BaseTypedString("value")
366
+
367
+ assert plain_typed_value == "value"
368
+ assert type(plain_typed_value) is BaseTypedString
369
+ ```
370
+
371
+ ---
372
+
373
+ ## Pydantic v2 support
374
+
375
+ When used as a Pydantic field type:
376
+
377
+ * validation accepts strict strings
378
+ * runtime model values preserve the exact subtype
379
+ * exported payloads are plain strings
380
+
381
+ ```python
382
+ from pydantic import BaseModel
383
+
384
+ from base_typed_string import BaseTypedString
385
+
386
+
387
+ class EmailAddress(BaseTypedString):
388
+ pass
389
+
390
+
391
+ class ContactModel(BaseModel):
392
+ primary_email: EmailAddress
393
+ backup_email: EmailAddress
394
+
395
+
396
+ contact_model: ContactModel = ContactModel.model_validate(
397
+ {
398
+ "primary_email": "primary@example.com",
399
+ "backup_email": "backup@example.com",
400
+ }
401
+ )
402
+
403
+ assert type(contact_model.primary_email) is EmailAddress
404
+ assert type(contact_model.backup_email) is EmailAddress
405
+
406
+ dumped_python: dict[str, object] = contact_model.model_dump()
407
+
408
+ assert dumped_python == {
409
+ "primary_email": "primary@example.com",
410
+ "backup_email": "backup@example.com",
411
+ }
412
+ assert type(dumped_python["primary_email"]) is str
413
+ ```
414
+
415
+ ### Important boundary
416
+
417
+ Inside the validated model, the exact subtype is preserved.
418
+
419
+ After serialization or export, values intentionally become plain strings.
420
+
421
+ This is a feature, not a bug.
422
+
423
+ ---
424
+
425
+ ## Pickle support
426
+
427
+ Pickle roundtrip preserves the exact subtype.
428
+
429
+ ```python
430
+ import pickle
431
+
432
+ from base_typed_string import BaseTypedString
433
+
434
+
435
+ class EmailAddress(BaseTypedString):
436
+ pass
437
+
438
+
439
+ source_email: EmailAddress = EmailAddress("hello@example.com")
440
+ serialized_email: bytes = pickle.dumps(source_email)
441
+ restored_email: object = pickle.loads(serialized_email)
442
+
443
+ assert restored_email == "hello@example.com"
444
+ assert type(restored_email) is EmailAddress
445
+ ```
446
+
447
+ ---
448
+
449
+ ## Public API
450
+
451
+ ```python
452
+ from base_typed_string import BaseTypedString
453
+ from base_typed_string import BaseTypedStringError
454
+ from base_typed_string import BaseTypedStringInvalidInputValueError
455
+ from base_typed_string import BaseTypedStringInvariantViolationError
456
+ ```
457
+
458
+ ### Exceptions
459
+
460
+ #### `BaseTypedStringError`
461
+
462
+ Root exception for all package-specific errors.
463
+
464
+ #### `BaseTypedStringInvalidInputValueError`
465
+
466
+ Raised when a non-string input value is provided.
467
+
468
+ #### `BaseTypedStringInvariantViolationError`
469
+
470
+ Raised when an internal invariant or contract is violated.
471
+
472
+ ---
473
+
474
+ ## Design notes
475
+
476
+ `BaseTypedString` is intended for projects that want domain-specific names without giving up normal `str` runtime behavior.
477
+
478
+ This is especially useful when you have many semantic string types such as:
479
+
480
+ * `AccountKey`
481
+ * `PromptKeyStr`
482
+ * `RawInputStr`
483
+ * `IntegrationName`
484
+ * `UserTextInputStr`
485
+ * `ValidatedInputStr`
486
+
487
+ The base class stays intentionally small so that your domain layer remains explicit and predictable.
488
+
489
+ ---
490
+
491
+ ## Development
492
+
493
+ ### Run tests
494
+
495
+ ```bash
496
+ pytest
497
+ ```
498
+
499
+ ### Run lint
500
+
501
+ ```bash
502
+ ruff check .
503
+ ```
504
+
505
+ ### Run type checking
506
+
507
+ ```bash
508
+ mypy .
509
+ pyright
510
+ ```
511
+
512
+ ### Build package
513
+
514
+ ```bash
515
+ python -m build
516
+ ```
517
+
518
+ ### Validate distribution metadata
519
+
520
+ ```bash
521
+ twine check dist/*
522
+ ```
523
+
524
+ ---
525
+
526
+ ## Compatibility
527
+
528
+ * Python 3.10+
529
+ * CPython
530
+ * optional Pydantic v2 support
531
+
532
+ ---
533
+
534
+ ## License
535
+
536
+ MIT