csrd-models 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.
- csrd/models/__init__.py +21 -0
- csrd/models/_base_model.py +9 -0
- csrd/models/_base_settings.py +12 -0
- csrd/models/claims.py +20 -0
- csrd/models/errors.py +71 -0
- csrd/models/message.py +5 -0
- csrd/models/model_parser/__init__.py +12 -0
- csrd/models/model_parser/_default_model_extractor.py +17 -0
- csrd/models/model_parser/_types.py +6 -0
- csrd/models/model_parser/model_parser_mixin.py +76 -0
- csrd/models/model_parser/payload_extractor_protocol.py +5 -0
- csrd/models/py.typed +0 -0
- csrd_models-0.1.0.dist-info/METADATA +11 -0
- csrd_models-0.1.0.dist-info/RECORD +15 -0
- csrd_models-0.1.0.dist-info/WHEEL +4 -0
csrd/models/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from . import model_parser
|
|
2
|
+
from ._base_model import BaseModel, model_config
|
|
3
|
+
from ._base_settings import BaseSettings, settings_config
|
|
4
|
+
from .claims import UserClaims
|
|
5
|
+
from .errors import APIErrorResponse, APIVersion, Error, ErrorMeta, SerializerMixin
|
|
6
|
+
from .message import Message
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
"APIErrorResponse",
|
|
10
|
+
"APIVersion",
|
|
11
|
+
"BaseModel",
|
|
12
|
+
"BaseSettings",
|
|
13
|
+
"Error",
|
|
14
|
+
"ErrorMeta",
|
|
15
|
+
"Message",
|
|
16
|
+
"SerializerMixin",
|
|
17
|
+
"UserClaims",
|
|
18
|
+
"model_config",
|
|
19
|
+
"model_parser",
|
|
20
|
+
"settings_config",
|
|
21
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pydantic import BaseModel as _BaseModel
|
|
2
|
+
from pydantic import ConfigDict
|
|
3
|
+
from pydantic.alias_generators import to_camel
|
|
4
|
+
|
|
5
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseModel(_BaseModel):
|
|
9
|
+
model_config = model_config
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings as _BaseSettings
|
|
2
|
+
from pydantic_settings import SettingsConfigDict
|
|
3
|
+
|
|
4
|
+
settings_config = SettingsConfigDict(
|
|
5
|
+
env_file=(".default_env", ".env"), env_file_encoding="utf-8", case_sensitive=False
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseSettings(_BaseSettings, frozen=True): # type: ignore[misc]
|
|
10
|
+
model_config = SettingsConfigDict(
|
|
11
|
+
env_file=(".default_env", ".env"), env_file_encoding="utf-8", case_sensitive=False
|
|
12
|
+
)
|
csrd/models/claims.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import UTC, datetime, timedelta
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class UserClaims:
|
|
7
|
+
"""Generic user claims for API authentication."""
|
|
8
|
+
|
|
9
|
+
sub: str = ""
|
|
10
|
+
user_name: str = ""
|
|
11
|
+
authorities: list[str] = field(default_factory=list)
|
|
12
|
+
iat: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
13
|
+
exp: datetime | None = None
|
|
14
|
+
|
|
15
|
+
def __post_init__(self) -> None:
|
|
16
|
+
if self.exp is None:
|
|
17
|
+
self.exp = self.iat + timedelta(hours=1)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ("UserClaims",)
|
csrd/models/errors.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Minimal REST API error models extracted from REST API error models."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import ConfigDict, TypeAdapter
|
|
8
|
+
from pydantic.alias_generators import to_camel
|
|
9
|
+
|
|
10
|
+
CONFIG = ConfigDict(alias_generator=to_camel, populate_by_name=True, arbitrary_types_allowed=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SerializerMixin:
|
|
14
|
+
def as_dict(self, **kwargs: Any) -> dict[str, Any]:
|
|
15
|
+
result = TypeAdapter(type(self)).dump_python(self, **kwargs)
|
|
16
|
+
return dict(result) # type: ignore[arg-type]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class APIVersion:
|
|
21
|
+
"""API version information for error responses."""
|
|
22
|
+
|
|
23
|
+
requested: str | None = None
|
|
24
|
+
served: str | None = None
|
|
25
|
+
|
|
26
|
+
__pydantic_config__ = CONFIG
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ErrorMeta:
|
|
31
|
+
"""Metadata for error responses."""
|
|
32
|
+
|
|
33
|
+
status: int = field(metadata={"description": "HTTP status code"})
|
|
34
|
+
error: str = field(metadata={"description": "Error message"})
|
|
35
|
+
path: str = field(metadata={"description": "Request path"})
|
|
36
|
+
api_version: APIVersion = field(metadata={"description": "API version information"})
|
|
37
|
+
timestamp: datetime = field(
|
|
38
|
+
default_factory=lambda: datetime.now(UTC), metadata={"description": "Error timestamp"}
|
|
39
|
+
)
|
|
40
|
+
method: str | None = field(default=None, metadata={"description": "HTTP method"})
|
|
41
|
+
request_id: str | None = field(
|
|
42
|
+
default=None, metadata={"description": "Unique request identifier"}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__pydantic_config__ = CONFIG
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Error:
|
|
50
|
+
"""Individual error details."""
|
|
51
|
+
|
|
52
|
+
title: str = field(metadata={"description": "Error title"})
|
|
53
|
+
detail: str | None = field(default=None, metadata={"description": "Detailed error description"})
|
|
54
|
+
code: str | None = field(
|
|
55
|
+
default=None, metadata={"description": "Application-specific error code"}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
__pydantic_config__ = CONFIG
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class APIErrorResponse(SerializerMixin):
|
|
63
|
+
"""Standard API error response format."""
|
|
64
|
+
|
|
65
|
+
meta: ErrorMeta = field(metadata={"description": "Error metadata"})
|
|
66
|
+
errors: list[Error] = field(metadata={"description": "List of errors"})
|
|
67
|
+
|
|
68
|
+
__pydantic_config__ = CONFIG
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = ("APIErrorResponse", "APIVersion", "Error", "ErrorMeta", "SerializerMixin")
|
csrd/models/message.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from ._default_model_extractor import DefaultExtractor
|
|
2
|
+
from ._types import ParsedResponse, ResponseModelType
|
|
3
|
+
from .model_parser_mixin import ModelParserMixin
|
|
4
|
+
from .payload_extractor_protocol import PayloadExtractor
|
|
5
|
+
|
|
6
|
+
__all__ = (
|
|
7
|
+
"DefaultExtractor",
|
|
8
|
+
"ModelParserMixin",
|
|
9
|
+
"ParsedResponse",
|
|
10
|
+
"PayloadExtractor",
|
|
11
|
+
"ResponseModelType",
|
|
12
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .payload_extractor_protocol import PayloadExtractor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DefaultExtractor(PayloadExtractor):
|
|
7
|
+
def extract(self, source: Any):
|
|
8
|
+
if hasattr(source, "json") and callable(source.json):
|
|
9
|
+
result = source.json()
|
|
10
|
+
# Guard against .json() returning a string (e.g. httpx Response)
|
|
11
|
+
# — callers expect a dict or list, not a raw JSON string.
|
|
12
|
+
if isinstance(result, str):
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
return json.loads(result)
|
|
16
|
+
return result
|
|
17
|
+
return source
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, get_args, get_origin
|
|
3
|
+
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
|
|
6
|
+
from ._default_model_extractor import DefaultExtractor
|
|
7
|
+
from ._types import ParsedResponse, ResponseModelType
|
|
8
|
+
from .payload_extractor_protocol import PayloadExtractor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ModelParserMixin:
|
|
12
|
+
"""
|
|
13
|
+
A mixin that applies a Pydantic model to various input formats,
|
|
14
|
+
with optional support for custom model handlers.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
_extractor: PayloadExtractor | None
|
|
18
|
+
|
|
19
|
+
def __init__(self, *, extractor: PayloadExtractor | None = None):
|
|
20
|
+
self._extractor = extractor
|
|
21
|
+
|
|
22
|
+
def apply_model(
|
|
23
|
+
self,
|
|
24
|
+
source: Any,
|
|
25
|
+
*,
|
|
26
|
+
model: ResponseModelType | None = None,
|
|
27
|
+
model_handler: Callable | None = None,
|
|
28
|
+
exception: type[Exception] | None = None,
|
|
29
|
+
extractor: PayloadExtractor | None = None,
|
|
30
|
+
) -> ParsedResponse:
|
|
31
|
+
"""
|
|
32
|
+
Convert source data into a parsed model or passthrough object.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
source: Input data (dict, list, or raw object) to parse.
|
|
36
|
+
model: Optional model class or List[Model] to parse against.
|
|
37
|
+
model_handler: Optional custom callable to handle parsing.
|
|
38
|
+
exception: Optional exception type to raise on failure.
|
|
39
|
+
extractor: Optional extraction handler callable to handle parsing.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
ParsedResponse: The parsed object(s) or raw source.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
if model_handler:
|
|
46
|
+
return model_handler(source, model)
|
|
47
|
+
|
|
48
|
+
extractor = extractor or self._extractor
|
|
49
|
+
data = self._resolve_payload(source, extractor)
|
|
50
|
+
|
|
51
|
+
if model is None:
|
|
52
|
+
return data
|
|
53
|
+
|
|
54
|
+
if get_origin(model) is list:
|
|
55
|
+
item_type = get_args(model)[0]
|
|
56
|
+
if not isinstance(data, list):
|
|
57
|
+
raise TypeError("Expected list in input payload")
|
|
58
|
+
return [item_type(**item) for item in data]
|
|
59
|
+
|
|
60
|
+
if not isinstance(data, dict):
|
|
61
|
+
raise TypeError("Expected dict in input payload")
|
|
62
|
+
|
|
63
|
+
return model(**data)
|
|
64
|
+
|
|
65
|
+
except (ValidationError, TypeError, ValueError, KeyError) as e:
|
|
66
|
+
if exception:
|
|
67
|
+
raise exception(e) from e
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _resolve_payload(source: Any, extractor: PayloadExtractor | None = None) -> Any:
|
|
72
|
+
"""
|
|
73
|
+
Uses a pluggable extractor to transform source into raw payload.
|
|
74
|
+
"""
|
|
75
|
+
extractor = extractor or DefaultExtractor()
|
|
76
|
+
return extractor.extract(source)
|
csrd/models/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csrd-models
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Base Pydantic models and settings
|
|
5
|
+
Project-URL: Repository, https://github.com/csrd-api/fastapi-common
|
|
6
|
+
Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/models
|
|
7
|
+
Project-URL: Changelog, https://github.com/csrd-api/fastapi-common/blob/main/CHANGELOG.md
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: pydantic-settings<3,>=2.10
|
|
11
|
+
Requires-Dist: pydantic<3,>=2.10
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
csrd/models/__init__.py,sha256=ZjTDQXisCEZ2FWm6jTOiyJbd-GajV5I9H7J0fR1590o,521
|
|
2
|
+
csrd/models/_base_model.py,sha256=hqlwp6Xvbe3AV4PGhqMt7S6o7HiSVDPWx-mWIvt78JE,263
|
|
3
|
+
csrd/models/_base_settings.py,sha256=dYGHQ6dr2gp21B4eJvNQ-b48AAuB8xPKse3ylu1vE2w,445
|
|
4
|
+
csrd/models/claims.py,sha256=s7Yx3SROiA9Wmhu3ov08sWZDm4JGNBnMAmxJiFr9ip0,521
|
|
5
|
+
csrd/models/errors.py,sha256=IhQSmVo8h5_26PHaOZRug-_y5ALegGLg5GZAD4hhlT4,2238
|
|
6
|
+
csrd/models/message.py,sha256=6wzt2px_i-5sGi0GcA7V-O-kaPZmPAbl8XPDpV2TOyM,79
|
|
7
|
+
csrd/models/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
csrd/models/model_parser/__init__.py,sha256=7tSycR6qSnkb1ewy69s689A1m6ZXuGjM3_SHVbbo_b8,349
|
|
9
|
+
csrd/models/model_parser/_default_model_extractor.py,sha256=V3EChNhrSs0jUj8S78EW1hI2q54XDyScHUr7W9MBNV8,566
|
|
10
|
+
csrd/models/model_parser/_types.py,sha256=jpfOJ1BytmDqTGzD_S1k7DCScDHtMJx-GMNq-v29sIw,178
|
|
11
|
+
csrd/models/model_parser/model_parser_mixin.py,sha256=svUvcJa1X6duDC8mQEcYAlFoYisGPAUqWSiALWDDXdY,2575
|
|
12
|
+
csrd/models/model_parser/payload_extractor_protocol.py,sha256=7jcmd87Gq37Sc2IFkRwXySVvn64Ch9WrLE60Y9FizHw,116
|
|
13
|
+
csrd_models-0.1.0.dist-info/METADATA,sha256=WLntD8xdwrxkZUB9fXIko04FshENwoAcMHxZkq6qiss,465
|
|
14
|
+
csrd_models-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
15
|
+
csrd_models-0.1.0.dist-info/RECORD,,
|