sotkalib 0.0.2__tar.gz → 0.0.4__tar.gz
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.
- {sotkalib-0.0.2 → sotkalib-0.0.4}/PKG-INFO +3 -1
- {sotkalib-0.0.2 → sotkalib-0.0.4}/pyproject.toml +6 -1
- sotkalib-0.0.4/src/sotkalib/__init__.py +3 -0
- sotkalib-0.0.4/src/sotkalib/config/field.py +11 -0
- sotkalib-0.0.4/src/sotkalib/config/struct.py +179 -0
- sotkalib-0.0.4/src/sotkalib/enum/__init__.py +3 -0
- sotkalib-0.0.4/src/sotkalib/enum/mixins.py +59 -0
- sotkalib-0.0.4/src/sotkalib/exceptions/__init__.py +3 -0
- sotkalib-0.0.4/src/sotkalib/exceptions/api/__init__.py +1 -0
- sotkalib-0.0.4/src/sotkalib/exceptions/api/exc.py +53 -0
- sotkalib-0.0.4/src/sotkalib/exceptions/handlers/__init__.py +4 -0
- sotkalib-0.0.4/src/sotkalib/exceptions/handlers/args_incl_error.py +15 -0
- sotkalib-0.0.4/src/sotkalib/exceptions/handlers/core.py +33 -0
- sotkalib-0.0.4/src/sotkalib/http/__init__.py +17 -0
- sotkalib-0.0.4/src/sotkalib/http/client_session.py +258 -0
- sotkalib-0.0.4/src/sotkalib/log/factory.py +12 -0
- sotkalib-0.0.4/src/sotkalib/redis/__init__.py +8 -0
- sotkalib-0.0.4/src/sotkalib/redis/client.py +38 -0
- sotkalib-0.0.4/src/sotkalib/redis/lock.py +82 -0
- sotkalib-0.0.4/src/sotkalib/sqla/__init__.py +3 -0
- sotkalib-0.0.4/src/sotkalib/sqla/db.py +101 -0
- sotkalib-0.0.2/src/sotkalib/__init__.py +0 -5
- sotkalib-0.0.2/src/sotkalib/config/field.py +0 -27
- sotkalib-0.0.2/src/sotkalib/config/struct.py +0 -179
- sotkalib-0.0.2/src/sotkalib/http/__init__.py +0 -17
- sotkalib-0.0.2/src/sotkalib/http/client_session.py +0 -247
- sotkalib-0.0.2/src/sotkalib/log/factory.py +0 -29
- {sotkalib-0.0.2 → sotkalib-0.0.4}/README.md +0 -0
- {sotkalib-0.0.2 → sotkalib-0.0.4}/src/sotkalib/config/__init__.py +0 -0
- {sotkalib-0.0.2 → sotkalib-0.0.4}/src/sotkalib/log/__init__.py +0 -0
- {sotkalib-0.0.2 → sotkalib-0.0.4}/src/sotkalib/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sotkalib
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary:
|
|
5
5
|
Author: alexey
|
|
6
6
|
Author-email: alexey <me@pyrorhythm.dev>
|
|
@@ -8,6 +8,8 @@ Requires-Dist: aiohttp>=3.13.3
|
|
|
8
8
|
Requires-Dist: dotenv>=0.9.9
|
|
9
9
|
Requires-Dist: loguru>=0.7.3
|
|
10
10
|
Requires-Dist: pydantic>=2.12.5
|
|
11
|
+
Requires-Dist: redis>=7.1.0
|
|
12
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.46
|
|
11
13
|
Requires-Python: >=3.13
|
|
12
14
|
Description-Content-Type: text/markdown
|
|
13
15
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sotkalib"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.4"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = [
|
|
6
6
|
{ email = "me@pyrorhythm.dev", name = "alexey" }
|
|
@@ -12,6 +12,8 @@ dependencies = [
|
|
|
12
12
|
"dotenv>=0.9.9",
|
|
13
13
|
"loguru>=0.7.3",
|
|
14
14
|
"pydantic>=2.12.5",
|
|
15
|
+
"redis>=7.1.0",
|
|
16
|
+
"sqlalchemy[asyncio]>=2.0.46",
|
|
15
17
|
]
|
|
16
18
|
|
|
17
19
|
|
|
@@ -36,7 +38,9 @@ dev = [
|
|
|
36
38
|
"mypy>=1.19.0",
|
|
37
39
|
"pyrefly>=0.45.2",
|
|
38
40
|
"pytest>=9.0.2",
|
|
41
|
+
"pytest-asyncio>=1.3.0",
|
|
39
42
|
"ruff>=0.14.9",
|
|
43
|
+
"testcontainers>=4.14.1",
|
|
40
44
|
]
|
|
41
45
|
|
|
42
46
|
[tool.ruff]
|
|
@@ -47,6 +51,7 @@ src = ["src"]
|
|
|
47
51
|
[tool.ruff.format]
|
|
48
52
|
docstring-code-format = true
|
|
49
53
|
line-ending = "lf"
|
|
54
|
+
indent-style = "tab"
|
|
50
55
|
|
|
51
56
|
[tool.ruff.lint]
|
|
52
57
|
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
type AllowedTypes = int | float | complex | str | bool | None
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(init=True, slots=True, frozen=True)
|
|
8
|
+
class SettingsField[T: AllowedTypes]:
|
|
9
|
+
default: T | None = None
|
|
10
|
+
factory: Callable[[], T] | str | None = None
|
|
11
|
+
nullable: bool = False
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from os import PathLike, getenv
|
|
6
|
+
from types import NoneType, UnionType
|
|
7
|
+
from typing import TYPE_CHECKING, Any, get_args
|
|
8
|
+
from warnings import warn
|
|
9
|
+
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
from sotkalib.log import get_logger
|
|
13
|
+
|
|
14
|
+
from .field import AllowedTypes, SettingsField
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from logging import (
|
|
18
|
+
Logger as StdLogger,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from loguru import Logger as LoguruLogger
|
|
22
|
+
|
|
23
|
+
type _loggers = StdLogger | LoguruLogger
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class AppSettings:
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
Base class for reading typed settings from environment variables.
|
|
31
|
+
|
|
32
|
+
Declare attributes with type annotations and assign SettingsField(...) to each.
|
|
33
|
+
On initialization, values are resolved from the environment (loading .env if provided),
|
|
34
|
+
then from default/factory, or set to None if nullable.
|
|
35
|
+
|
|
36
|
+
**Notes:**
|
|
37
|
+
|
|
38
|
+
Only immutable primitive types are allowed: int, float, complex, str, bool, None.
|
|
39
|
+
|
|
40
|
+
If explicit_format is True, attribute names must be UPPER_SNAKE_CASE.
|
|
41
|
+
|
|
42
|
+
**Example:**
|
|
43
|
+
|
|
44
|
+
>>> import secrets
|
|
45
|
+
>>> class MySettings(AppSettings):
|
|
46
|
+
... BOT_TOKEN: str = SettingsField(nullable=False)
|
|
47
|
+
... POSTGRES_USER: str = SettingsField(default="pg_user")
|
|
48
|
+
... POSTGRES_PASSWORD: str = SettingsField(nullable=False, factory=lambda: secrets.token_urlsafe(8))
|
|
49
|
+
... SECRET_ALIAS: str = SettingsField(factory="secret")
|
|
50
|
+
...
|
|
51
|
+
... @property
|
|
52
|
+
... def secret(self) -> str:
|
|
53
|
+
... return "computed"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
>>> settings = MySettings()
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
dotenv_path: str | PathLike[str] | None = None,
|
|
63
|
+
logger: _loggers | None = None,
|
|
64
|
+
explicit_format: bool = True,
|
|
65
|
+
strict: bool = False,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
Initialize AppSettings and resolve annotated fields.
|
|
70
|
+
|
|
71
|
+
**Parameters:**
|
|
72
|
+
|
|
73
|
+
- `dotenv_path`: Optional path to a .env file. If None, python-dotenv searches recursively.
|
|
74
|
+
- `logger`: Optional log or loguru logger.
|
|
75
|
+
- `explicit_format`: If True, attribute names must be uppercase with underscores.
|
|
76
|
+
- `strict`: If True, any mutable types will raise an exception rather than being set to
|
|
77
|
+
None.
|
|
78
|
+
|
|
79
|
+
**Raises:**
|
|
80
|
+
|
|
81
|
+
- `AttributeError`: If an attribute name violates explicit_format constraint or a declared property is missing.
|
|
82
|
+
- `TypeError`: If an annotation uses a disallowed (mutable) type or a factory reference is not a property.
|
|
83
|
+
- `ValueError`: If a required field (nullable=False, no default/factory) is missing in the environment.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def _unwrap_type(tp: type) -> type:
|
|
88
|
+
if isinstance(tp, UnionType):
|
|
89
|
+
args = [a for a in get_args(tp) if a is not NoneType]
|
|
90
|
+
return args[0] if args else NoneType
|
|
91
|
+
return tp
|
|
92
|
+
|
|
93
|
+
def evaluate_var(_type: type, _var: str) -> Any:
|
|
94
|
+
_type = _unwrap_type(_type)
|
|
95
|
+
if _type is NoneType:
|
|
96
|
+
return None
|
|
97
|
+
if _type is bool:
|
|
98
|
+
return _var.lower() in ("yes", "true", "1", "y")
|
|
99
|
+
return _type(_var)
|
|
100
|
+
|
|
101
|
+
load_dotenv(dotenv_path=dotenv_path)
|
|
102
|
+
|
|
103
|
+
self.__log = get_logger("utilities.appsettings") if logger is None else logger
|
|
104
|
+
self.__deferred = []
|
|
105
|
+
self.__strict = strict
|
|
106
|
+
|
|
107
|
+
cls_annotations = self.__class__.__annotations__
|
|
108
|
+
cls_dict = self.__class__.__dict__
|
|
109
|
+
|
|
110
|
+
settings_fields: dict[str, SettingsField] = {
|
|
111
|
+
attr: val for attr, val in cls_dict.items() if isinstance(val, SettingsField)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for attr, settings_field in settings_fields.items():
|
|
115
|
+
if explicit_format and not re.fullmatch(r"[A-Z][A-Z0-9_]*", attr):
|
|
116
|
+
raise AttributeError("AppSettings attributes should contain only capital letters and underscores")
|
|
117
|
+
|
|
118
|
+
annotated = cls_annotations.get(attr, NoneType)
|
|
119
|
+
string_value = getenv(attr, None)
|
|
120
|
+
|
|
121
|
+
if string_value is None:
|
|
122
|
+
self.__validate_empty_string_value(attr, settings_field)
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
typed_value = evaluate_var(annotated, string_value)
|
|
126
|
+
|
|
127
|
+
setattr(self, attr, self.__validate(typed_value, strict=self.__strict))
|
|
128
|
+
self.__log.debug(f"evaluated {attr} from environment")
|
|
129
|
+
|
|
130
|
+
self.__post_init__()
|
|
131
|
+
|
|
132
|
+
def __validate_empty_string_value(self, attr: str, settings_field: SettingsField) -> None:
|
|
133
|
+
if settings_field.default is not None:
|
|
134
|
+
setattr(self, attr, self.__validate(settings_field.default, strict=self.__strict))
|
|
135
|
+
self.__log.debug(f"evaluated {attr} from default")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if settings_field.factory is not None:
|
|
139
|
+
if isinstance(settings_field.factory, str):
|
|
140
|
+
self.__deferred.append((attr, settings_field.factory))
|
|
141
|
+
self.__log.debug(f"defer {attr} init as factory is a str; => property")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
if callable(settings_field.factory):
|
|
145
|
+
setattr(self, attr, self.__validate(settings_field.factory(), strict=self.__strict))
|
|
146
|
+
self.__log.debug(f"evaluated {attr} from factory")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
raise TypeError(f"unknown type for a factory: {type(settings_field.factory)}")
|
|
150
|
+
|
|
151
|
+
if settings_field.nullable:
|
|
152
|
+
setattr(self, attr, None)
|
|
153
|
+
self.__log.debug(f"evaluated {attr} as None (nullable)")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
raise ValueError(f"reqd field {attr} was not found in .env")
|
|
157
|
+
|
|
158
|
+
def __post_init__(self) -> None:
|
|
159
|
+
for attr, factory in self.__deferred:
|
|
160
|
+
if factory not in self.__class__.__dict__:
|
|
161
|
+
raise AttributeError(f"property {factory} was not found in {self.__class__.__name__}")
|
|
162
|
+
if not isinstance(getattr(self.__class__, factory), property):
|
|
163
|
+
raise TypeError(f"method {factory} is not a property")
|
|
164
|
+
self.__log.debug(f"evaluated {attr} from property {factory}")
|
|
165
|
+
setattr(self, attr, self.__validate(getattr(self, factory), strict=self.__strict))
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def __validate[T: Any](val: T, strict: bool) -> T | None:
|
|
169
|
+
typeval = type(val)
|
|
170
|
+
allowed = get_args(AllowedTypes.__value__)
|
|
171
|
+
|
|
172
|
+
if typeval not in allowed:
|
|
173
|
+
if strict:
|
|
174
|
+
raise TypeError(f"{typeval} is not an allowed immutable type")
|
|
175
|
+
else:
|
|
176
|
+
warn(f"{typeval} is mutable, setting value to None", stacklevel=2)
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
return val
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Literal, Self, overload
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UppercaseStrEnumMixin(str, Enum):
|
|
7
|
+
@staticmethod
|
|
8
|
+
def _generate_next_value_(name: str, start: int, count: int, last_values: Sequence) -> str: # noqa
|
|
9
|
+
return name.upper()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ValidatorStrEnumMixin(str, Enum):
|
|
13
|
+
@classmethod
|
|
14
|
+
def _normalize_value(cls, val: Any) -> str:
|
|
15
|
+
if isinstance(val, (str, bytes, bytearray)):
|
|
16
|
+
return val.decode("utf-8") if isinstance(val, (bytes, bytearray)) else val
|
|
17
|
+
raise TypeError("value must be str-like")
|
|
18
|
+
|
|
19
|
+
@overload
|
|
20
|
+
@classmethod
|
|
21
|
+
def validate(cls, *, val: Any, req: Literal[False] = False) -> Self | None: ...
|
|
22
|
+
|
|
23
|
+
@overload
|
|
24
|
+
@classmethod
|
|
25
|
+
def validate(cls, *, val: Any, req: Literal[True]) -> Self: ...
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def validate(cls, *, val: Any, req: bool = False) -> Self | None:
|
|
29
|
+
if val is None:
|
|
30
|
+
if req:
|
|
31
|
+
raise ValueError("value is None and req=True")
|
|
32
|
+
return None
|
|
33
|
+
normalized = cls._normalize_value(val)
|
|
34
|
+
try:
|
|
35
|
+
return cls(normalized)
|
|
36
|
+
except ValueError as e:
|
|
37
|
+
raise TypeError(f"{normalized=} not valid: {e}") from e
|
|
38
|
+
|
|
39
|
+
@overload
|
|
40
|
+
@classmethod
|
|
41
|
+
def get(cls, val: Any, default: Literal[None] = None) -> Self | None: ...
|
|
42
|
+
|
|
43
|
+
@overload
|
|
44
|
+
@classmethod
|
|
45
|
+
def get(cls, val: Any, default: Self) -> Self: ...
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get(cls, val: Any, default: Self | None = None) -> Self | None:
|
|
49
|
+
try:
|
|
50
|
+
return cls.validate(val=val, req=False) or default
|
|
51
|
+
except (ValueError, TypeError):
|
|
52
|
+
return default
|
|
53
|
+
|
|
54
|
+
def in_(self, *enum_values: Self) -> bool:
|
|
55
|
+
return self in enum_values
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def values(cls) -> Sequence[Self]:
|
|
59
|
+
return list(cls)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .exc import APIError, ErrorSchema
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import http
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ErrorSchema(BaseModel):
|
|
9
|
+
code: str | None = None
|
|
10
|
+
desc: str | None = None
|
|
11
|
+
ctx: Mapping[str, Any] | str | list[Any] | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseHTTPError(Exception):
|
|
15
|
+
def __init__(self, status_code: int, detail: str | None = None, headers: Mapping[str, str] | None = None) -> None:
|
|
16
|
+
if detail is None:
|
|
17
|
+
detail = http.HTTPStatus(status_code).phrase
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.detail = detail
|
|
20
|
+
self.headers = headers
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
return f"{self.status_code}: {self.detail}"
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
class_name = self.__class__.__name__
|
|
27
|
+
return f"{class_name}(status_code={self.status_code!r}, detail={self.detail!r})"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class APIError(BaseHTTPError):
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
status: http.HTTPStatus | int = http.HTTPStatus.BAD_REQUEST,
|
|
35
|
+
code: str | None = None,
|
|
36
|
+
desc: str | None = None,
|
|
37
|
+
ctx: Mapping[str, Any] | list[Any] | str | None = None,
|
|
38
|
+
):
|
|
39
|
+
if isinstance(status, int):
|
|
40
|
+
status = http.HTTPStatus(status)
|
|
41
|
+
|
|
42
|
+
self.status = status
|
|
43
|
+
self.code = code
|
|
44
|
+
self.desc = desc
|
|
45
|
+
self.ctx = ctx
|
|
46
|
+
|
|
47
|
+
self.schema = ErrorSchema(
|
|
48
|
+
code=self.code,
|
|
49
|
+
desc=self.desc,
|
|
50
|
+
ctx=self.ctx,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
super().__init__(status_code=self.status.value, detail=self.schema.model_dump_json())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ArgsIncludedError(Exception):
|
|
5
|
+
def __init__(self, *args, stack_depth: int = 2):
|
|
6
|
+
_args = args
|
|
7
|
+
stack_args_to_exc = []
|
|
8
|
+
frames = inspect.stack()[1:-1][::-1][:stack_depth]
|
|
9
|
+
for frame_info in frames:
|
|
10
|
+
frame = frame_info.frame
|
|
11
|
+
args, _, _, values = inspect.getargvalues(frame)
|
|
12
|
+
f_locals = frame.f_locals
|
|
13
|
+
args_with_values = {arg: values[arg] for arg in args}
|
|
14
|
+
stack_args_to_exc.append(args_with_values | f_locals | { "frame_name": frame.f_code.co_name })
|
|
15
|
+
super().__init__(*_args, *stack_args_to_exc)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from collections.abc import Callable, Coroutine
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .args_incl_error import ArgsIncludedError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def exception_handler[**P, R](
|
|
9
|
+
func: Callable[P, R],
|
|
10
|
+
stack_depth: int = 3,
|
|
11
|
+
) -> Callable[P, R]:
|
|
12
|
+
@wraps(func)
|
|
13
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
14
|
+
try:
|
|
15
|
+
return func(*args, **kwargs)
|
|
16
|
+
except Exception as e:
|
|
17
|
+
raise ArgsIncludedError(*e.args, stack_depth=stack_depth) from e
|
|
18
|
+
|
|
19
|
+
return wrapper
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def aexception_handler[**P, R](
|
|
23
|
+
func: Callable[P, Coroutine[Any, Any, R]],
|
|
24
|
+
stack_depth: int = 7,
|
|
25
|
+
) -> Callable[P, Coroutine[Any, Any, R]]:
|
|
26
|
+
@wraps(func)
|
|
27
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
28
|
+
try:
|
|
29
|
+
return await func(*args, **kwargs)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise ArgsIncludedError(*e.args, stack_depth=stack_depth) from e
|
|
32
|
+
|
|
33
|
+
return wrapper
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .client_session import (
|
|
2
|
+
ClientSettings,
|
|
3
|
+
ExceptionSettings,
|
|
4
|
+
Handler,
|
|
5
|
+
HTTPSession,
|
|
6
|
+
Middleware,
|
|
7
|
+
StatusSettings,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = (
|
|
11
|
+
"HTTPSession",
|
|
12
|
+
"ExceptionSettings",
|
|
13
|
+
"StatusSettings",
|
|
14
|
+
"ClientSettings",
|
|
15
|
+
"Handler",
|
|
16
|
+
"Middleware",
|
|
17
|
+
)
|