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.
@@ -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,5 @@
1
+ from ._base_model import BaseModel
2
+
3
+
4
+ class Message(BaseModel):
5
+ detail: str
@@ -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,6 @@
1
+ from typing import Any, TypeVar
2
+
3
+ T = TypeVar("T")
4
+
5
+ ResponseModelType = type[T]
6
+ type ParsedResponse = Any # T | list[T] | bytes | dict — not expressible as a static type alias
@@ -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)
@@ -0,0 +1,5 @@
1
+ from typing import Any, Protocol
2
+
3
+
4
+ class PayloadExtractor(Protocol):
5
+ def extract(self, source: Any) -> Any: ...
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any