msgspec-extras 0.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.
@@ -0,0 +1,33 @@
1
+ __all__ = [
2
+ "FutureDate",
3
+ "FutureDatetime",
4
+ "HttpUrl",
5
+ "NegativeFloat",
6
+ "NegativeInt",
7
+ "NonNegativeFloat",
8
+ "NonNegativeInt",
9
+ "NonPositiveFloat",
10
+ "NonPositiveInt",
11
+ "PastDate",
12
+ "PastDatetime",
13
+ "PositiveFloat",
14
+ "PositiveInt",
15
+ "__version__",
16
+ "dec_hook",
17
+ "enc_hook",
18
+ ]
19
+ __version__ = "0.1.0"
20
+
21
+ from msgspec_extras._hooks import dec_hook, enc_hook
22
+ from msgspec_extras.datetime import FutureDate, FutureDatetime, PastDate, PastDatetime
23
+ from msgspec_extras.numeric import (
24
+ NegativeFloat,
25
+ NegativeInt,
26
+ NonNegativeFloat,
27
+ NonNegativeInt,
28
+ NonPositiveFloat,
29
+ NonPositiveInt,
30
+ PositiveFloat,
31
+ PositiveInt,
32
+ )
33
+ from msgspec_extras.string import HttpUrl
@@ -0,0 +1,17 @@
1
+ __all__ = ["dec_hook", "enc_hook"]
2
+
3
+ from typing import Any
4
+
5
+ from msgspec_extras.datetime import FutureDate, FutureDatetime, PastDate, PastDatetime
6
+
7
+
8
+ def dec_hook(typ: type, obj: Any) -> Any: # noqa: ANN401
9
+ if typ in (PastDate, FutureDate, PastDatetime, FutureDatetime):
10
+ return typ(obj)
11
+ raise NotImplementedError(f"Type {typ.__name__} unsupported in dec_hook")
12
+
13
+
14
+ def enc_hook(obj: Any) -> Any: # noqa: ANN401
15
+ if isinstance(obj, (PastDate, FutureDate, PastDatetime, FutureDatetime)):
16
+ return obj.isoformat()
17
+ raise NotImplementedError(f"Encoding objects of type {type(obj).__name__} is unsupported")
@@ -0,0 +1,106 @@
1
+ __all__ = ["FutureDate", "FutureDatetime", "PastDate", "PastDatetime"]
2
+
3
+ from datetime import date, datetime
4
+
5
+ from msgspec import ValidationError
6
+
7
+ try:
8
+ from typing import Self # ty:ignore[unresolved-import]
9
+ except ImportError:
10
+ from typing_extensions import Self
11
+
12
+
13
+ class PastDate(date):
14
+ __slots__ = ()
15
+
16
+ def __new__(cls, value: date | str) -> Self:
17
+ if isinstance(value, date):
18
+ tmp: date = super().__new__(cls, value.year, value.month, value.day)
19
+ elif isinstance(value, str):
20
+ try:
21
+ tmp: date = date.fromisoformat(value)
22
+ except ValueError as err:
23
+ raise ValueError(f"Invalid date format: {value}") from err
24
+ else:
25
+ raise TypeError(f"PastDate must be a date or string, got {type(value).__name__}")
26
+ if tmp < date.today():
27
+ return tmp
28
+ raise ValidationError(f"PastDate must be in the past, got {tmp}")
29
+
30
+
31
+ class FutureDate(date):
32
+ __slots__ = ()
33
+
34
+ def __new__(cls, value: date | str) -> Self:
35
+ if isinstance(value, date):
36
+ tmp: date = super().__new__(cls, value.year, value.month, value.day)
37
+ elif isinstance(value, str):
38
+ try:
39
+ tmp: date = date.fromisoformat(value)
40
+ except ValueError as err:
41
+ raise ValueError(f"Invalid date format: {value}") from err
42
+ else:
43
+ raise TypeError(f"FutureDate must be a date or string, got {type(value).__name__}")
44
+ if tmp > date.today():
45
+ return tmp
46
+ raise ValidationError(f"FutureDate must be in the future, got {tmp}")
47
+
48
+
49
+ class PastDatetime(datetime):
50
+ __slots__ = ()
51
+
52
+ def __new__(cls, value: datetime | str) -> Self:
53
+ if isinstance(value, datetime):
54
+ tmp: datetime = super().__new__(
55
+ cls,
56
+ value.year,
57
+ value.month,
58
+ value.day,
59
+ value.hour,
60
+ value.minute,
61
+ value.second,
62
+ value.microsecond,
63
+ value.tzinfo,
64
+ )
65
+ elif isinstance(value, str):
66
+ try:
67
+ tmp: datetime = datetime.fromisoformat(value)
68
+ except ValueError as err:
69
+ raise ValueError(f"Invalid datetime format: {value}") from err
70
+ else:
71
+ raise TypeError(
72
+ f"PastDatetime must be a datetime or string, got {type(value).__name__}"
73
+ )
74
+ if tmp < datetime.now():
75
+ return tmp
76
+ raise ValidationError(f"PastDatetime must be in the past, got {tmp}")
77
+
78
+
79
+ class FutureDatetime(datetime):
80
+ __slots__ = ()
81
+
82
+ def __new__(cls, value: datetime | str) -> Self:
83
+ if isinstance(value, datetime):
84
+ tmp: datetime = super().__new__(
85
+ cls,
86
+ value.year,
87
+ value.month,
88
+ value.day,
89
+ value.hour,
90
+ value.minute,
91
+ value.second,
92
+ value.microsecond,
93
+ value.tzinfo,
94
+ )
95
+ elif isinstance(value, str):
96
+ try:
97
+ tmp: datetime = datetime.fromisoformat(value)
98
+ except ValueError as err:
99
+ raise ValueError(f"Invalid datetime format: {value}") from err
100
+ else:
101
+ raise TypeError(
102
+ f"FutureDatetime must be a datetime or string, got {type(value).__name__}"
103
+ )
104
+ if tmp > datetime.now():
105
+ return tmp
106
+ raise ValidationError(f"FutureDatetime must be in the future, got {tmp}")
@@ -0,0 +1,24 @@
1
+ __all__ = [
2
+ "NegativeFloat",
3
+ "NegativeInt",
4
+ "NonNegativeFloat",
5
+ "NonNegativeInt",
6
+ "NonPositiveFloat",
7
+ "NonPositiveInt",
8
+ "PositiveFloat",
9
+ "PositiveInt",
10
+ ]
11
+
12
+ from typing import Annotated
13
+
14
+ from msgspec import Meta
15
+
16
+ PositiveInt = Annotated[int, Meta(gt=0, description="Integer greater than 0")]
17
+ NegativeInt = Annotated[int, Meta(lt=0, description="Integer less than 0")]
18
+ NonNegativeInt = Annotated[int, Meta(ge=0, description="Integer greater than or equal to 0")]
19
+ NonPositiveInt = Annotated[int, Meta(le=0, description="Integer less than or equal to 0")]
20
+
21
+ PositiveFloat = Annotated[float, Meta(gt=0.0, description="Float greater than 0.0")]
22
+ NegativeFloat = Annotated[float, Meta(lt=0.0, description="Float less than 0.0")]
23
+ NonNegativeFloat = Annotated[float, Meta(ge=0.0, description="Float greater than or equal to 0.0")]
24
+ NonPositiveFloat = Annotated[float, Meta(le=0.0, description="Float less than or equal to 0.0")]
@@ -0,0 +1,7 @@
1
+ __all__ = ["HttpUrl"]
2
+
3
+ from typing import Annotated
4
+
5
+ from msgspec import Meta
6
+
7
+ HttpUrl = Annotated[str, Meta(max_length=2083, pattern=r"^https?://[^\s/$.?#].[^\s]*$")]
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: msgspec-extras
3
+ Version: 0.1.0
4
+ Project-URL: Homepage, https://pypi.org/project/msgspec-extras
5
+ Project-URL: Issues, https://codefloe.com/buriedincode/msgspec-extras/issues
6
+ Project-URL: Source, https://codefloe.com/buriedincode/msgspec-extras
7
+ Author-email: BuriedInCode <buriedincode@duckpond.nz>
8
+ Maintainer-email: BuriedInCode <buriedincode@duckpond.nz>
9
+ License-Expression: MIT
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
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 :: 3.14
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: msgspec>=0.21.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # msgspec-extras
26
+
27
+ [![PyPI - Python](https://img.shields.io/pypi/pyversions/msgspec-extras.svg?logo=Python&label=Python&style=flat-square)](https://pypi.org/p/msgspec-extras/)
28
+ [![PyPI - Status](https://img.shields.io/pypi/status/msgspec-extras.svg?logo=Python&label=Status&style=flat-square)](https://pypi.org/p/msgspec-extras/)
29
+ [![PyPI - Version](https://img.shields.io/pypi/v/msgspec-extras.svg?logo=Python&label=Version&style=flat-square)](https://pypi.org/p/msgspec-extras/)
30
+ [![PyPI - License](https://img.shields.io/pypi/l/msgspec-extras.svg?logo=Python&label=License&style=flat-square)](https://opensource.org/licenses/MIT)
31
+
32
+ [![prek](https://img.shields.io/badge/prek-enabled-informational?logo=prek&style=flat-square)](https://github.com/j178/prek)
33
+ [![Ruff](https://img.shields.io/badge/Ruff-enabled-informational?logo=ruff&style=flat-square)](https://github.com/astral-sh/ruff)
34
+ [![ty](https://img.shields.io/badge/ty-enabled-informational?logo=ruff&style=flat-square)](https://github.com/astral-sh/ty)
35
+
36
+ [![status-badge](https://ci.codefloe.com/api/v1/badges/1187/status.svg)](https://ci.codefloe.com/repos/1187)
37
+
38
+ A small collection of types that extend [msgspec](https://msgspec.dev)'s built-in type support.
39
+
40
+ ## Installation
41
+
42
+ ```sh
43
+ pdm add msgspec-extras
44
+ ```
45
+
46
+ ## Type Support
47
+
48
+ `msgspec-extras` supports **all msgspec native types** - see the full list in the [msgspec documentation](https://msgspec.dev/supported-types).
49
+ It also adds the following custom types.
50
+
51
+ ### Datetime
52
+
53
+ - `PastDate` - must be earlier than today
54
+ - `PastDatetime` - must be earlier than now
55
+ - `FutureDate` - must be later than today
56
+ - `FutureDatetime` - must be later than now
57
+
58
+ These types require the `enc_hook`/`dec_hook` to be passed to msgspec's encode/decode calls.
59
+
60
+ ```python
61
+ from datetime import datetime, date
62
+
63
+ import msgspec
64
+ from msgspec_extras import PastDate, PastDatetime, FutureDate, FutureDatetime, enc_hook, dec_hook
65
+
66
+ past_date = msgspec.json.decode('"2020-03-21"', type=PastDate, dec_hook=dec_hook)
67
+ past_date_json = msgspec.json.encode(past_date, enc_hook=enc_hook)
68
+
69
+ msgspec.json.decode('"2020-03-21"', type=FutureDate, dec_hook=dec_hook)
70
+ # Raises `msgspec.ValidationError` as the date isn't in the future
71
+ ```
72
+
73
+ ### Numeric
74
+
75
+ | Type | Constraint |
76
+ | ------------------ | ---------- |
77
+ | `PositiveInt` | `> 0` |
78
+ | `PositiveFloat` | `> 0` |
79
+ | `NonNegativeInt` | `>= 0` |
80
+ | `NonNegativeFloat` | `>= 0` |
81
+ | `NegativeInt` | `< 0` |
82
+ | `NegativeFloat` | `< 0` |
83
+ | `NonPositiveInt` | `<= 0` |
84
+ | `NonPositiveFloat` | `<= 0` |
85
+
86
+ ### String
87
+
88
+ - `HttpUrl` - must be a valid HTTP or HTTPS URL
89
+
90
+ ## Usage
91
+
92
+ The numeric and string types are just `Annotated` aliases over msgspec's native types (e.g. `Annotated[int, msgspec.Meta(gt=0)]`), so they need no hooks and work as drop-in replacements anywhere you'd use `int`, `float`, or `str`:
93
+
94
+ ```python
95
+ import msgspec
96
+ from msgspec_extras import PositiveFloat
97
+
98
+
99
+ class Product(msgspec.Struct):
100
+ name: str
101
+ price: PositiveFloat
102
+
103
+
104
+ product = msgspec.json.decode(b'{"name": "Widget", "price": 5.1}', type=Product)
105
+ msgspec.json.encode(product)
106
+ ```
107
+
108
+ ## Socials
109
+
110
+ [![Social - Matrix](https://img.shields.io/matrix/The-Dev-Environment:matrix.org?label=The-Dev-Environment&logo=matrix&style=for-the-badge)](https://matrix.to/#/#The-Dev-Environment:matrix.org)
@@ -0,0 +1,8 @@
1
+ msgspec_extras/__init__.py,sha256=xQYXPuLq2Zmfyd3iApxVEsFGvpEXiFHMVg0CBT9hw1A,728
2
+ msgspec_extras/_hooks.py,sha256=j2zi2eWXkETou0D_WMBTm8cEhbYxOCe_hhx28DDfOE8,630
3
+ msgspec_extras/datetime.py,sha256=a5cWqlS6cuf_IKCTRwzz9pAJOvC4csP-swzE8DWtK0c,3570
4
+ msgspec_extras/numeric.py,sha256=ldUYGCEkiE9b3hLmpt8zOx1YO0FY2EKraw8A0Kr3_no,948
5
+ msgspec_extras/string.py,sha256=FNMf6Epc2Qu6pK7qXvGSgMOO85qZErKDy2hdzJo2UVg,168
6
+ msgspec_extras-0.1.0.dist-info/METADATA,sha256=jONtMT3J-x--MpWRmF-Icx78YnyAiJsaNQaTydrfc4o,4345
7
+ msgspec_extras-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
8
+ msgspec_extras-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any