nexo-schemas 0.0.16__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.
Files changed (69) hide show
  1. nexo/schemas/__init__.py +0 -0
  2. nexo/schemas/application.py +292 -0
  3. nexo/schemas/connection.py +134 -0
  4. nexo/schemas/data.py +27 -0
  5. nexo/schemas/document.py +237 -0
  6. nexo/schemas/error/__init__.py +476 -0
  7. nexo/schemas/error/constants.py +50 -0
  8. nexo/schemas/error/descriptor.py +354 -0
  9. nexo/schemas/error/enums.py +40 -0
  10. nexo/schemas/error/metadata.py +15 -0
  11. nexo/schemas/error/spec.py +312 -0
  12. nexo/schemas/exception/__init__.py +0 -0
  13. nexo/schemas/exception/exc.py +911 -0
  14. nexo/schemas/exception/factory.py +1928 -0
  15. nexo/schemas/exception/handlers.py +110 -0
  16. nexo/schemas/google.py +14 -0
  17. nexo/schemas/key/__init__.py +0 -0
  18. nexo/schemas/key/rsa.py +131 -0
  19. nexo/schemas/metadata.py +21 -0
  20. nexo/schemas/mixins/__init__.py +0 -0
  21. nexo/schemas/mixins/filter.py +140 -0
  22. nexo/schemas/mixins/general.py +65 -0
  23. nexo/schemas/mixins/hierarchy.py +19 -0
  24. nexo/schemas/mixins/identity.py +387 -0
  25. nexo/schemas/mixins/parameter.py +50 -0
  26. nexo/schemas/mixins/service.py +40 -0
  27. nexo/schemas/mixins/sort.py +111 -0
  28. nexo/schemas/mixins/timestamp.py +192 -0
  29. nexo/schemas/model.py +240 -0
  30. nexo/schemas/operation/__init__.py +0 -0
  31. nexo/schemas/operation/action/__init__.py +9 -0
  32. nexo/schemas/operation/action/base.py +14 -0
  33. nexo/schemas/operation/action/resource.py +371 -0
  34. nexo/schemas/operation/action/status.py +8 -0
  35. nexo/schemas/operation/action/system.py +6 -0
  36. nexo/schemas/operation/action/websocket.py +6 -0
  37. nexo/schemas/operation/base.py +289 -0
  38. nexo/schemas/operation/constants.py +18 -0
  39. nexo/schemas/operation/context.py +68 -0
  40. nexo/schemas/operation/dependency.py +26 -0
  41. nexo/schemas/operation/enums.py +168 -0
  42. nexo/schemas/operation/extractor.py +36 -0
  43. nexo/schemas/operation/mixins.py +53 -0
  44. nexo/schemas/operation/request.py +1066 -0
  45. nexo/schemas/operation/resource.py +839 -0
  46. nexo/schemas/operation/system.py +55 -0
  47. nexo/schemas/operation/websocket.py +55 -0
  48. nexo/schemas/pagination.py +67 -0
  49. nexo/schemas/parameter.py +60 -0
  50. nexo/schemas/payload.py +116 -0
  51. nexo/schemas/resource.py +64 -0
  52. nexo/schemas/response.py +1041 -0
  53. nexo/schemas/security/__init__.py +0 -0
  54. nexo/schemas/security/api_key.py +63 -0
  55. nexo/schemas/security/authentication.py +848 -0
  56. nexo/schemas/security/authorization.py +922 -0
  57. nexo/schemas/security/enums.py +32 -0
  58. nexo/schemas/security/impersonation.py +179 -0
  59. nexo/schemas/security/token.py +402 -0
  60. nexo/schemas/security/types.py +17 -0
  61. nexo/schemas/success/__init__.py +0 -0
  62. nexo/schemas/success/descriptor.py +100 -0
  63. nexo/schemas/success/enums.py +23 -0
  64. nexo/schemas/user_agent.py +46 -0
  65. nexo_schemas-0.0.16.dist-info/METADATA +87 -0
  66. nexo_schemas-0.0.16.dist-info/RECORD +69 -0
  67. nexo_schemas-0.0.16.dist-info/WHEEL +5 -0
  68. nexo_schemas-0.0.16.dist-info/licenses/LICENSE +21 -0
  69. nexo_schemas-0.0.16.dist-info/top_level.txt +1 -0
@@ -0,0 +1,110 @@
1
+ from typing import Awaitable, Callable
2
+ from fastapi import Request, Response, status
3
+ from fastapi.encoders import jsonable_encoder
4
+ from fastapi.exceptions import (
5
+ HTTPException,
6
+ RequestValidationError,
7
+ WebSocketRequestValidationError,
8
+ ResponseValidationError,
9
+ )
10
+ from fastapi.requests import HTTPConnection
11
+ from fastapi.responses import JSONResponse
12
+ from pydantic import ValidationError
13
+ from starlette.authentication import AuthenticationError
14
+ from nexo.logging.logger import Exception as ExceptionLogger
15
+ from nexo.utils.exception import extract_details
16
+ from ..error.constants import ERROR_CODE_STATUS_CODE_MAP
17
+ from ..error.enums import ErrorCode
18
+ from ..google import ListOfPublisherHandlers
19
+ from ..response import (
20
+ UnauthorizedResponse,
21
+ UnprocessableEntityResponse,
22
+ InternalServerErrorResponse,
23
+ ErrorResponseFactory,
24
+ )
25
+ from .exc import MaleoException, AnyException
26
+
27
+
28
+ def authentication_error_handler(
29
+ conn: HTTPConnection, exc: AuthenticationError
30
+ ) -> JSONResponse:
31
+ return JSONResponse(
32
+ content=UnauthorizedResponse(other=extract_details(exc)).model_dump(
33
+ mode="json"
34
+ ),
35
+ status_code=status.HTTP_401_UNAUTHORIZED,
36
+ )
37
+
38
+
39
+ async def validation_error_handler(
40
+ request: Request,
41
+ exc: (
42
+ RequestValidationError
43
+ | WebSocketRequestValidationError
44
+ | ResponseValidationError
45
+ | ValidationError
46
+ ),
47
+ ) -> JSONResponse:
48
+ return JSONResponse(
49
+ content=UnprocessableEntityResponse(
50
+ other=jsonable_encoder(exc.errors())
51
+ ).model_dump(mode="json"),
52
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
53
+ )
54
+
55
+
56
+ async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
57
+ return JSONResponse(
58
+ content=(
59
+ ErrorResponseFactory.cls_from_code(exc.status_code)(
60
+ other=extract_details(exc)
61
+ ).model_dump(mode="json")
62
+ ),
63
+ status_code=exc.status_code,
64
+ )
65
+
66
+
67
+ def create_maleo_exception_handler(
68
+ logger: ExceptionLogger,
69
+ publishers: ListOfPublisherHandlers = [],
70
+ ) -> Callable[[Request, MaleoException], Awaitable[JSONResponse]]:
71
+ async def maleo_exception_handler(
72
+ request: Request, exc: MaleoException | AnyException
73
+ ) -> JSONResponse:
74
+ exc.log_and_publish_operation(logger, publishers)
75
+
76
+ return JSONResponse(
77
+ content=exc.response.model_dump(mode="json"),
78
+ status_code=exc.error.spec.status_code,
79
+ )
80
+
81
+ return maleo_exception_handler
82
+
83
+
84
+ async def general_exception_handler(request: Request, exc: Exception) -> Response:
85
+ other = extract_details(exc)
86
+
87
+ # Get the first arg as a potential ErrorCode
88
+ code = exc.args[0] if exc.args else None
89
+
90
+ if isinstance(code, ErrorCode):
91
+ error_code = code
92
+ elif isinstance(code, str) and code in ErrorCode:
93
+ error_code = ErrorCode[code]
94
+ else:
95
+ error_code = None
96
+
97
+ if error_code is not None:
98
+ status_code = ERROR_CODE_STATUS_CODE_MAP.get(error_code, None)
99
+
100
+ if status_code is not None:
101
+ response_cls = ErrorResponseFactory.cls_from_code(status_code)
102
+ return JSONResponse(
103
+ content=response_cls(other=other).model_dump(mode="json"),
104
+ status_code=status_code,
105
+ )
106
+
107
+ return JSONResponse(
108
+ content=InternalServerErrorResponse(other=other).model_dump(mode="json"),
109
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
110
+ )
nexo/schemas/google.py ADDED
@@ -0,0 +1,14 @@
1
+ from google.cloud.pubsub_v1 import PublisherClient
2
+ from pydantic import BaseModel, ConfigDict, Field
3
+ from typing import Annotated
4
+
5
+
6
+ class PublisherHandler(BaseModel):
7
+ model_config = ConfigDict(arbitrary_types_allowed=True)
8
+
9
+ client: Annotated[PublisherClient, Field(..., description="Publisher client")]
10
+ project_id: Annotated[str, Field(..., description="Project ID")]
11
+ topic_id: Annotated[str, Field(..., description="Topic ID")]
12
+
13
+
14
+ ListOfPublisherHandlers = list[PublisherHandler]
File without changes
@@ -0,0 +1,131 @@
1
+ from abc import ABC, abstractmethod
2
+ from Crypto.Cipher import PKCS1_OAEP
3
+ from Crypto.PublicKey import RSA
4
+ from functools import cached_property
5
+ from pydantic import BaseModel, Field, model_validator
6
+ from typing import Annotated, Self
7
+ from nexo.crypto.key.rsa.enums import KeyType
8
+ from nexo.crypto.key.rsa.loader import with_cryptography
9
+ from nexo.types.misc import PathOrStr
10
+ from nexo.types.string import OptStr
11
+
12
+
13
+ class Key(BaseModel, ABC):
14
+ raw: Annotated[str, Field(..., description="Raw key")]
15
+
16
+ @abstractmethod
17
+ def _validate_raw(self):
18
+ """Validate raw key"""
19
+
20
+ @model_validator(mode="after")
21
+ def validate_raw(self) -> Self:
22
+ self._validate_raw()
23
+ return self
24
+
25
+ @cached_property
26
+ def as_rsa(self) -> RSA.RsaKey:
27
+ """Convert raw to RSA"""
28
+ self._validate_raw()
29
+ passphrase = getattr(self, "password", None)
30
+ return RSA.import_key(extern_key=self.raw, passphrase=passphrase)
31
+
32
+
33
+ class Private(Key):
34
+ password: Annotated[OptStr, Field(None, description="Key's password")] = None
35
+
36
+ def _validate_raw(self):
37
+ if not RSA.import_key(
38
+ extern_key=self.raw, passphrase=self.password
39
+ ).has_private():
40
+ raise ValueError(
41
+ "Invalid key type, the private key did not have private inside it"
42
+ )
43
+
44
+
45
+ class Public(Key):
46
+ def _validate_raw(self):
47
+ if RSA.import_key(extern_key=self.raw).has_private():
48
+ raise ValueError("Invalid key type, the public key had private inside it")
49
+
50
+
51
+ class Keys(BaseModel):
52
+ private: Annotated[Private, Field(..., description="Private key")]
53
+ public: Annotated[Public, Field(..., description="Public key")]
54
+
55
+ @model_validator(mode="after")
56
+ def validate_complete_keys(self) -> Self:
57
+ try:
58
+ # Import private key with password
59
+ private_key = self.private.as_rsa
60
+
61
+ # Import public key
62
+ public_key = self.public.as_rsa
63
+
64
+ # Validate keys match by comparing public components
65
+ if (
66
+ private_key.publickey().n != public_key.n
67
+ or private_key.publickey().e != public_key.e
68
+ ):
69
+ raise ValueError("Public key does not match the private key")
70
+
71
+ # Optional: Test encrypt/decrypt functionality
72
+ test_message = b"validation_test"
73
+ try:
74
+ # Encrypt with public key
75
+ cipher = PKCS1_OAEP.new(public_key)
76
+ encrypted = cipher.encrypt(test_message)
77
+
78
+ # Decrypt with private key
79
+ cipher = PKCS1_OAEP.new(private_key)
80
+ decrypted = cipher.decrypt(encrypted)
81
+
82
+ if decrypted != test_message:
83
+ raise ValueError(
84
+ "Keys do not work together for encryption/decryption"
85
+ )
86
+
87
+ except Exception as e:
88
+ raise ValueError(f"Keys failed encryption/decryption test: {str(e)}")
89
+
90
+ except ValueError:
91
+ raise # Re-raise validation errors
92
+ except Exception as e:
93
+ raise ValueError(f"Key validation failed: {str(e)}")
94
+
95
+ return self
96
+
97
+ @classmethod
98
+ def from_path(
99
+ cls,
100
+ private: PathOrStr,
101
+ public: PathOrStr,
102
+ password: OptStr = None,
103
+ ) -> Self:
104
+ raw_private = with_cryptography(
105
+ KeyType.PRIVATE,
106
+ path=private,
107
+ password=password,
108
+ )
109
+ raw_public = with_cryptography(
110
+ KeyType.PUBLIC,
111
+ path=public,
112
+ )
113
+ return cls(
114
+ private=Private(raw=raw_private, password=password),
115
+ public=Public(raw=raw_public),
116
+ )
117
+
118
+ @classmethod
119
+ def from_string(
120
+ cls,
121
+ private: str,
122
+ public: str,
123
+ password: OptStr = None,
124
+ ) -> Self:
125
+ return cls(
126
+ private=Private(raw=private, password=password), public=Public(raw=public)
127
+ )
128
+
129
+
130
+ class KeysMixin(BaseModel):
131
+ keys: Annotated[Keys, Field(..., description="RSA Keys")]
@@ -0,0 +1,21 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Generic, TypeVar
3
+ from .mixins.general import Success, Descriptor, Other
4
+
5
+
6
+ AnyMetadataT = TypeVar("AnyMetadataT")
7
+ ModelMetadataT = TypeVar("ModelMetadataT", bound=BaseModel | None)
8
+
9
+
10
+ class MetadataMixin(BaseModel, Generic[AnyMetadataT]):
11
+ metadata: AnyMetadataT = Field(..., description="Metadata")
12
+
13
+
14
+ class FieldExpansionMetadata(Other, Descriptor[str], Success[bool]):
15
+ pass
16
+
17
+
18
+ class FieldExpansionMetadataMixin(BaseModel):
19
+ field_expansion: str | dict[str, FieldExpansionMetadata] | None = Field(
20
+ None, description="Field expansion metadata"
21
+ )
File without changes
@@ -0,0 +1,140 @@
1
+ import re
2
+ from datetime import datetime
3
+ from pydantic import BaseModel, Field, field_validator, model_validator
4
+ from typing import Annotated, Self, TypeGuard, overload
5
+ from nexo.types.datetime import OptDatetime
6
+ from nexo.types.string import ListOfStrs
7
+ from .identity import Name
8
+ from .timestamp import FromTimestamp, ToTimestamp
9
+
10
+
11
+ DATE_FILTER_REGEX = (
12
+ r"^(?P<name>[a-z_]+)"
13
+ r"(?:\|from::(?P<from>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})))?"
14
+ r"(?:\|to::(?P<to>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})))?$"
15
+ )
16
+ DATE_FILTER_PATTERN = re.compile(DATE_FILTER_REGEX)
17
+
18
+
19
+ class DateFilter(
20
+ ToTimestamp[OptDatetime],
21
+ FromTimestamp[OptDatetime],
22
+ Name[str],
23
+ ):
24
+ name: Annotated[str, Field(..., description="Name", min_length=1)]
25
+ from_date: Annotated[OptDatetime, Field(None, description="From date")] = None
26
+ to_date: Annotated[OptDatetime, Field(None, description="To date")] = None
27
+
28
+ def _validate_timestamps(self):
29
+ if self.from_date is None and self.to_date is None:
30
+ raise ValueError("Either 'from_date' or 'to_date' must have value")
31
+ if self.from_date is not None and self.to_date is not None:
32
+ if self.to_date < self.from_date:
33
+ raise ValueError("Attribute 'from_date' can not be more than 'to_date'")
34
+
35
+ @model_validator(mode="after")
36
+ def validate_timestamps(self) -> Self:
37
+ self._validate_timestamps()
38
+ return self
39
+
40
+ @classmethod
41
+ def from_string(cls, filter: str) -> "DateFilter":
42
+ match = DATE_FILTER_PATTERN.match(filter)
43
+ if not match:
44
+ raise ValueError(f"Invalid date filter format: {filter!r}")
45
+
46
+ name = match.group("name")
47
+ from_raw = match.group("from")
48
+ to_raw = match.group("to")
49
+
50
+ from_date = datetime.fromisoformat(from_raw) if from_raw else None
51
+ to_date = datetime.fromisoformat(to_raw) if to_raw else None
52
+
53
+ if from_date is None and to_date is None:
54
+ raise ValueError("Either 'from_date' or 'to_date' must be provided")
55
+
56
+ return cls(name=name, from_date=from_date, to_date=to_date)
57
+
58
+ def to_string(self) -> str:
59
+ self._validate_timestamps()
60
+ filter_string = self.name
61
+ if self.from_date:
62
+ filter_string += f"|from::{self.from_date.isoformat()}"
63
+ if self.to_date:
64
+ filter_string += f"|to::{self.to_date.isoformat()}"
65
+ return filter_string
66
+
67
+
68
+ OptDateFilter = DateFilter | None
69
+ ListOfDateFilters = list[DateFilter]
70
+ OptListOfDateFilters = OptDateFilter | None
71
+
72
+
73
+ AnyFilters = ListOfDateFilters | ListOfStrs
74
+
75
+
76
+ def is_date_filters(
77
+ filters: AnyFilters,
78
+ ) -> TypeGuard[ListOfDateFilters]:
79
+ return all(isinstance(filter, DateFilter) for filter in filters)
80
+
81
+
82
+ def is_filters(
83
+ filters: AnyFilters,
84
+ ) -> TypeGuard[ListOfStrs]:
85
+ return all(isinstance(filter, str) for filter in filters)
86
+
87
+
88
+ class Filters(BaseModel):
89
+ filters: Annotated[
90
+ ListOfStrs,
91
+ Field(
92
+ list[str](),
93
+ description="Date range filters with '<COLUMN_NAME>|from::<ISO_DATETIME>|to::<ISO_DATETIME>' format",
94
+ ),
95
+ ] = list[str]()
96
+
97
+ @field_validator("filters", mode="after")
98
+ @classmethod
99
+ def validate_filters_pattern(cls, value: ListOfStrs) -> ListOfStrs:
100
+ for v in value:
101
+ match = DATE_FILTER_PATTERN.match(v)
102
+ if not match:
103
+ raise ValueError(f"Invalid date filter format: {v!r}")
104
+ return value
105
+
106
+ @classmethod
107
+ def from_date_filters(cls, date_filters: ListOfDateFilters) -> "Filters":
108
+ return cls(filters=[filter.to_string() for filter in date_filters])
109
+
110
+ @property
111
+ def date_filters(self) -> ListOfDateFilters:
112
+ return [DateFilter.from_string(filter) for filter in self.filters]
113
+
114
+
115
+ class DateFilters(BaseModel):
116
+ date_filters: Annotated[
117
+ ListOfDateFilters,
118
+ Field(list[DateFilter](), description="Date filters to be applied"),
119
+ ] = list[DateFilter]()
120
+
121
+ @classmethod
122
+ def from_filters(cls, filters: ListOfStrs) -> "DateFilters":
123
+ return cls(date_filters=[DateFilter.from_string(filter) for filter in filters])
124
+
125
+ @property
126
+ def filters(self) -> ListOfStrs:
127
+ return [filter.to_string() for filter in self.date_filters]
128
+
129
+
130
+ @overload
131
+ def convert(filters: ListOfDateFilters) -> ListOfStrs: ...
132
+ @overload
133
+ def convert(filters: ListOfStrs) -> ListOfDateFilters: ...
134
+ def convert(filters: AnyFilters) -> AnyFilters:
135
+ if is_date_filters(filters):
136
+ return [filter.to_string() for filter in filters]
137
+ elif is_filters(filters):
138
+ return [DateFilter.from_string(filter) for filter in filters]
139
+ else:
140
+ raise ValueError("Filter type is neither DateFilter nor string")
@@ -0,0 +1,65 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Annotated, Any, Generic
3
+ from nexo.types.boolean import BoolT, OptBoolT
4
+ from nexo.types.enum import OptStrEnumT
5
+ from nexo.types.misc import (
6
+ OptFloatOrIntT,
7
+ OptIntOrStrEnumT,
8
+ OptStrOrStrEnumT,
9
+ OptListOfStrsOrStrEnumsT,
10
+ )
11
+ from nexo.types.string import OptStrT
12
+
13
+
14
+ class StatusCode(BaseModel):
15
+ status_code: Annotated[int, Field(..., description="Status code", ge=100, le=600)]
16
+
17
+
18
+ class Success(BaseModel, Generic[BoolT]):
19
+ success: BoolT = Field(..., description="Success")
20
+
21
+
22
+ class Code(BaseModel, Generic[OptStrOrStrEnumT]):
23
+ code: OptStrOrStrEnumT = Field(..., description="Code")
24
+
25
+
26
+ class Codes(BaseModel, Generic[OptListOfStrsOrStrEnumsT]):
27
+ codes: OptListOfStrsOrStrEnumsT = Field(..., description="Codes")
28
+
29
+
30
+ class Message(BaseModel):
31
+ message: str = Field(..., description="Message")
32
+
33
+
34
+ class Description(BaseModel):
35
+ description: str = Field(..., description="Description")
36
+
37
+
38
+ class Descriptor(
39
+ Description, Message, Code[OptStrOrStrEnumT], Generic[OptStrOrStrEnumT]
40
+ ):
41
+ pass
42
+
43
+
44
+ class Order(BaseModel, Generic[OptIntOrStrEnumT]):
45
+ order: OptIntOrStrEnumT = Field(..., description="Order")
46
+
47
+
48
+ class Level(BaseModel, Generic[OptStrEnumT]):
49
+ level: OptStrEnumT = Field(..., description="Level")
50
+
51
+
52
+ class Note(BaseModel, Generic[OptStrT]):
53
+ note: OptStrT = Field(..., description="Note")
54
+
55
+
56
+ class IsDefault(BaseModel, Generic[OptBoolT]):
57
+ is_default: OptBoolT = Field(..., description="Whether is default")
58
+
59
+
60
+ class Other(BaseModel):
61
+ other: Annotated[Any, Field(None, description="Other")] = None
62
+
63
+
64
+ class Age(BaseModel, Generic[OptFloatOrIntT]):
65
+ age: OptFloatOrIntT = Field(..., ge=0, description="Age")
@@ -0,0 +1,19 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Generic
3
+ from nexo.types.boolean import OptBoolT
4
+
5
+
6
+ class IsRoot(BaseModel, Generic[OptBoolT]):
7
+ is_root: OptBoolT = Field(..., description="Whether is root")
8
+
9
+
10
+ class IsParent(BaseModel, Generic[OptBoolT]):
11
+ is_parent: OptBoolT = Field(..., description="Whether is parent")
12
+
13
+
14
+ class IsChild(BaseModel, Generic[OptBoolT]):
15
+ is_child: OptBoolT = Field(..., description="Whether is child")
16
+
17
+
18
+ class IsLeaf(BaseModel, Generic[OptBoolT]):
19
+ is_leaf: OptBoolT = Field(..., description="Whether is leaf")