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.
- msgspec_extras/__init__.py +33 -0
- msgspec_extras/_hooks.py +17 -0
- msgspec_extras/datetime.py +106 -0
- msgspec_extras/numeric.py +24 -0
- msgspec_extras/string.py +7 -0
- msgspec_extras-0.1.0.dist-info/METADATA +110 -0
- msgspec_extras-0.1.0.dist-info/RECORD +8 -0
- msgspec_extras-0.1.0.dist-info/WHEEL +4 -0
|
@@ -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
|
msgspec_extras/_hooks.py
ADDED
|
@@ -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")]
|
msgspec_extras/string.py
ADDED
|
@@ -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
|
+
[](https://pypi.org/p/msgspec-extras/)
|
|
28
|
+
[](https://pypi.org/p/msgspec-extras/)
|
|
29
|
+
[](https://pypi.org/p/msgspec-extras/)
|
|
30
|
+
[](https://opensource.org/licenses/MIT)
|
|
31
|
+
|
|
32
|
+
[](https://github.com/j178/prek)
|
|
33
|
+
[](https://github.com/astral-sh/ruff)
|
|
34
|
+
[](https://github.com/astral-sh/ty)
|
|
35
|
+
|
|
36
|
+
[](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
|
+
[](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,,
|