sqlalchemy-declarative-filters 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,41 @@
1
+ """Declarative SQLAlchemy filters with auto-generated schemas.
2
+
3
+ Import ``Filters`` and the decorators from this package for the dependency-free
4
+ dataclass backend, or from ``.pydantic`` / ``.marshmallow`` for those. The names are
5
+ the same in all three, so switching a project over is a one-line change::
6
+
7
+ from sqlalchemy_declarative_filters.pydantic import Filters, options, skip_null
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from ._decorators import options, skip_null
13
+ from ._exceptions import (
14
+ BackendNotAvailableError,
15
+ FilterDeclarationError,
16
+ FilterError,
17
+ JoinConflictWarning,
18
+ UnknownFilterError,
19
+ )
20
+ from ._joins import Statement
21
+ from ._meta import Filters, FiltersMeta
22
+
23
+ __version__ = "0.1.0"
24
+
25
+ #: Explicit alias, for when more than one backend's base is in the same module.
26
+ DataclassFilters = Filters
27
+
28
+ __all__ = (
29
+ "BackendNotAvailableError",
30
+ "DataclassFilters",
31
+ "FilterDeclarationError",
32
+ "FilterError",
33
+ "Filters",
34
+ "FiltersMeta",
35
+ "JoinConflictWarning",
36
+ "Statement",
37
+ "UnknownFilterError",
38
+ "__version__",
39
+ "options",
40
+ "skip_null",
41
+ )
@@ -0,0 +1,112 @@
1
+ """Public typing surface for the dataclass namespace.
2
+
3
+ The implementation modules stay plain, annotated Python and are type-checked as such;
4
+ only the three namespace modules carry stubs, because their signatures are the part
5
+ that differs per backend and reads badly inline.
6
+ """
7
+
8
+ from collections.abc import Callable, Mapping
9
+ from typing import Any, TypeVar
10
+
11
+ from sqlalchemy.sql._typing import (
12
+ _ColumnExpressionArgument,
13
+ _JoinTargetArgument,
14
+ _OnClauseArgument,
15
+ )
16
+ from sqlalchemy.sql.selectable import Select
17
+
18
+ from ._spec import FilterSpec as FilterSpec
19
+
20
+ __version__: str
21
+
22
+ _FuncT = TypeVar("_FuncT", bound=Callable[..., Any])
23
+ _StatementT = TypeVar("_StatementT")
24
+
25
+ class FilterError(Exception): ...
26
+ class FilterDeclarationError(FilterError, TypeError): ...
27
+ class UnknownFilterError(FilterError, KeyError): ...
28
+ class BackendNotAvailableError(FilterError, ImportError): ...
29
+ class JoinConflictWarning(UserWarning): ...
30
+
31
+ class FiltersMeta(type):
32
+ #: The generated schema, in whichever backend ``__backend__`` names.
33
+ @property
34
+ def Schema(cls) -> type[Any]: ...
35
+ #: Alias of :attr:`Schema`.
36
+ @property
37
+ def Model(cls) -> type[Any]: ...
38
+ @property
39
+ def Dataclass(cls) -> type[Any]: ...
40
+ @property
41
+ def Pydantic(cls) -> type[Any]: ...
42
+ @property
43
+ def Marshmallow(cls) -> type[Any]: ...
44
+ @property
45
+ def __filters__(cls) -> tuple[FilterSpec, ...]: ...
46
+ def build_schema(cls, backend: str | None = ...) -> type[Any]: ...
47
+ def apply(
48
+ cls,
49
+ statement: _StatementT,
50
+ values: Mapping[str, Any] | Any | None = ...,
51
+ ) -> _StatementT: ...
52
+
53
+ class Statement:
54
+ """The statement being built, as a filter body sees it.
55
+
56
+ ``self`` inside a filter is one of these, and so is what a filter returns.
57
+ Everything a SQLAlchemy ``Select`` offers is forwarded. Only the handful of methods
58
+ a filter body genuinely needs are spelled out, because every name declared here is
59
+ a name a filter cannot have; ``__getattr__`` covers ``order_by``, ``distinct`` and
60
+ the rest of the statement's surface.
61
+ """
62
+
63
+ def where(self, *whereclause: _ColumnExpressionArgument[bool]) -> Statement: ...
64
+ def having(self, *having: _ColumnExpressionArgument[bool]) -> Statement: ...
65
+ def join(
66
+ self,
67
+ target: _JoinTargetArgument,
68
+ onclause: _OnClauseArgument | None = ...,
69
+ *,
70
+ isouter: bool = ...,
71
+ full: bool = ...,
72
+ ) -> Statement:
73
+ """Join ``target`` unless it is already joined.
74
+
75
+ Deduplicated against the joins the other filters in the same ``apply`` asked
76
+ for and against the ones the incoming statement already carried.
77
+ """
78
+
79
+ def outerjoin(
80
+ self,
81
+ target: _JoinTargetArgument,
82
+ onclause: _OnClauseArgument | None = ...,
83
+ *,
84
+ full: bool = ...,
85
+ ) -> Statement: ...
86
+ #: The underlying SQLAlchemy statement.
87
+ def unwrap(self) -> Select[Any]: ...
88
+ def __getattr__(self, name: str) -> Any: ...
89
+
90
+ class Filters(Statement, metaclass=FiltersMeta):
91
+ #: Which backend ``Schema`` uses; set by the base class you inherit from.
92
+ __backend__: str
93
+ #: Strings a query parameter may use to mean ``null`` on a ``@skip_null`` filter.
94
+ __null_strings__: frozenset[str]
95
+ #: Overrides the generated schema's class name.
96
+ __schema_name__: str
97
+
98
+ DataclassFilters = Filters
99
+
100
+ def options(
101
+ *,
102
+ default_factory: Callable[[], Any] = ...,
103
+ init: bool = ...,
104
+ repr: bool = ...,
105
+ hash: bool | None = ...,
106
+ compare: bool = ...,
107
+ metadata: Mapping[str, Any] = ...,
108
+ kw_only: bool = ...,
109
+ ) -> Callable[[_FuncT], _FuncT]:
110
+ """Keywords for :func:`dataclasses.field`, which is what backs this namespace."""
111
+
112
+ def skip_null(func: _FuncT) -> _FuncT: ...
@@ -0,0 +1,59 @@
1
+ """Schema backend registry.
2
+
3
+ Backends are imported the first time they are asked for, so importing this package
4
+ never pulls in Pydantic or Marshmallow.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from importlib import import_module
10
+ from typing import TYPE_CHECKING, cast
11
+
12
+ from .._exceptions import BackendNotAvailableError
13
+ from .base import SchemaBackend, SchemaRequest
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Iterator
17
+
18
+ __all__ = ("SchemaBackend", "SchemaRequest", "backend_names", "get_backend")
19
+
20
+ #: name -> (module, class, extra to install)
21
+ _BACKENDS: dict[str, tuple[str, str, str | None]] = {
22
+ "dataclass": (".dataclass", "DataclassBackend", None),
23
+ "pydantic": (".pydantic", "PydanticBackend", "pydantic"),
24
+ "marshmallow": (".marshmallow", "MarshmallowBackend", "marshmallow"),
25
+ }
26
+
27
+ _LOADED: dict[str, SchemaBackend] = {}
28
+
29
+
30
+ def backend_names() -> Iterator[str]:
31
+ """The names :func:`get_backend` accepts."""
32
+
33
+ return iter(_BACKENDS)
34
+
35
+
36
+ def get_backend(name: str) -> SchemaBackend:
37
+ """Return the backend called ``name``, importing it on first use."""
38
+
39
+ if (backend := _LOADED.get(name)) is not None:
40
+ return backend
41
+
42
+ try:
43
+ module_name, class_name, extra = _BACKENDS[name]
44
+ except KeyError:
45
+ known = ", ".join(sorted(_BACKENDS))
46
+ raise ValueError(f"Unknown schema backend {name!r}; expected one of {known}.") from None
47
+
48
+ try:
49
+ module = import_module(module_name, __name__)
50
+ except ImportError as exc:
51
+ raise BackendNotAvailableError(
52
+ f"The {name!r} schema backend needs a dependency that is not installed: "
53
+ f"{exc}. Install it with "
54
+ f"`pip install sqlalchemy-declarative-filters[{extra}]`."
55
+ ) from exc
56
+
57
+ backend = _LOADED[name] = cast("SchemaBackend", getattr(module, class_name)())
58
+
59
+ return backend
@@ -0,0 +1,56 @@
1
+ """The contract every schema backend implements."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Any, ClassVar
8
+
9
+ if TYPE_CHECKING:
10
+ from .._spec import FilterSpec
11
+
12
+ __all__ = ("SchemaBackend", "SchemaRequest")
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class SchemaRequest:
17
+ """Everything a backend needs to render one filter class as a schema."""
18
+
19
+ name: str
20
+ doc: str | None
21
+ module: str
22
+ specs: tuple[FilterSpec, ...]
23
+ null_strings: frozenset[str]
24
+
25
+ def is_null_string(self, value: Any) -> bool:
26
+ """Whether ``value`` is one of the strings that stand in for ``null``."""
27
+
28
+ return isinstance(value, str) and value.strip().lower() in self.null_strings
29
+
30
+ def coerce(self, value: Any) -> Any:
31
+ """Map a null-like string to ``None`` for filters that opted into it."""
32
+
33
+ return None if self.is_null_string(value) else value
34
+
35
+ @property
36
+ def nullable_names(self) -> tuple[str, ...]:
37
+ """Names of the filters whose null-like strings should become ``None``."""
38
+
39
+ return tuple(spec.name for spec in self.specs if spec.skip_null)
40
+
41
+
42
+ class SchemaBackend(ABC):
43
+ """Renders :class:`~.._spec.FilterSpec` objects into a schema class."""
44
+
45
+ #: The name this backend is selected by.
46
+ name: ClassVar[str]
47
+
48
+ #: The extra to install to get it, or ``None`` when it needs no dependency.
49
+ extra: ClassVar[str | None] = None
50
+
51
+ @abstractmethod
52
+ def build(self, request: SchemaRequest) -> type[Any]:
53
+ """Build and return the schema class for ``request``."""
54
+
55
+ def __repr__(self) -> str:
56
+ return f"<{type(self).__name__} {self.name!r}>"
@@ -0,0 +1,81 @@
1
+ """The default backend: a plain :mod:`dataclasses` dataclass, no dependencies.
2
+
3
+ It is a typed container, not a validator. Nothing checks that the values you put in
4
+ match their annotations, and ``@options`` here takes ``dataclasses.field`` keywords
5
+ rather than constraints. Use the Pydantic or Marshmallow backend when the values come
6
+ from outside your own code and need validating.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import dataclasses
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from .base import SchemaBackend
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Mapping
18
+
19
+ from .base import SchemaRequest
20
+
21
+ __all__ = ("DataclassBackend",)
22
+
23
+
24
+ class DataclassBackend(SchemaBackend):
25
+ """Renders filters as a keyword-only dataclass."""
26
+
27
+ name = "dataclass"
28
+
29
+ def build(self, request: SchemaRequest) -> type[Any]:
30
+ annotations: dict[str, Any] = {}
31
+ namespace: dict[str, Any] = {
32
+ "__annotations__": annotations,
33
+ "__doc__": request.doc,
34
+ "__module__": request.module,
35
+ "__filters_request__": request,
36
+ "from_mapping": classmethod(_from_mapping),
37
+ }
38
+
39
+ for spec in request.specs:
40
+ annotations[spec.name] = spec.schema_annotation
41
+ namespace[spec.name] = _field_for(spec)
42
+
43
+ # kw_only keeps declaration order regardless of which filters have defaults.
44
+ return dataclasses.dataclass(kw_only=True)(type(request.name, (), namespace))
45
+
46
+
47
+ def _field_for(spec: Any) -> Any:
48
+ """Build the ``dataclasses.field`` for one filter."""
49
+
50
+ options = dict(spec.options)
51
+ metadata = {"description": spec.doc, **options.pop("metadata", {})}
52
+
53
+ if "default_factory" in options:
54
+ # A mutable default has to come from a factory; it replaces the plain default.
55
+ return dataclasses.field(metadata=metadata, **options)
56
+
57
+ return dataclasses.field(default=spec.schema_default, metadata=metadata, **options)
58
+
59
+
60
+ def _from_mapping(cls: type[Any], data: Mapping[str, Any]) -> Any:
61
+ """Build an instance from a mapping, applying null-string coercion.
62
+
63
+ This is what the other backends get from their validation layer. It exists so a
64
+ dataclass schema can still be fed raw query parameters.
65
+ """
66
+
67
+ request: SchemaRequest = cls.__filters_request__
68
+ nullable = frozenset(request.nullable_names)
69
+ known = {field.name for field in dataclasses.fields(cls)}
70
+ unknown = set(data) - known
71
+
72
+ if unknown:
73
+ names = ", ".join(sorted(unknown))
74
+ raise TypeError(f"{cls.__name__} has no filter(s) named {names}.")
75
+
76
+ return cls(
77
+ **{
78
+ name: request.coerce(value) if name in nullable else value
79
+ for name, value in data.items()
80
+ }
81
+ )
@@ -0,0 +1,128 @@
1
+ """The Marshmallow backend: a ``Schema`` subclass whose ``load()`` returns a dict.
2
+
3
+ Marshmallow has no single field class, so annotations are mapped onto concrete field
4
+ types here. ``@options`` keywords go to that field's constructor -- ``validate``,
5
+ ``data_key``, ``required`` and the rest -- and constraints are expressed the
6
+ Marshmallow way, with validators::
7
+
8
+ @options(validate=validate.Length(min=3))
9
+ def name(self, value: str): ...
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import datetime
15
+ import decimal
16
+ import enum
17
+ import uuid
18
+ from typing import TYPE_CHECKING, Any, Literal, get_args, get_origin
19
+
20
+ from marshmallow import EXCLUDE, Schema, fields, pre_load, validate
21
+
22
+ from .._spec import unwrap_optional
23
+ from .base import SchemaBackend
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Mapping
27
+
28
+ from .._spec import FilterSpec
29
+ from .base import SchemaRequest
30
+
31
+ __all__ = ("MarshmallowBackend",)
32
+
33
+ _SCALARS: dict[Any, type[fields.Field[Any]]] = {
34
+ str: fields.String,
35
+ int: fields.Integer,
36
+ float: fields.Float,
37
+ bool: fields.Boolean,
38
+ decimal.Decimal: fields.Decimal,
39
+ datetime.datetime: fields.DateTime,
40
+ datetime.date: fields.Date,
41
+ datetime.time: fields.Time,
42
+ datetime.timedelta: fields.TimeDelta,
43
+ uuid.UUID: fields.UUID,
44
+ dict: fields.Dict,
45
+ }
46
+
47
+ _SEQUENCES = (list, set, frozenset, tuple)
48
+
49
+
50
+ class MarshmallowBackend(SchemaBackend):
51
+ """Renders filters as a Marshmallow schema."""
52
+
53
+ name = "marshmallow"
54
+ extra = "marshmallow"
55
+
56
+ def build(self, request: SchemaRequest) -> type[Schema]:
57
+ namespace: dict[str, Any] = {
58
+ "__doc__": request.doc,
59
+ "__module__": request.module,
60
+ # Filters are optional by nature; an unknown one is a caller mistake.
61
+ "Meta": type("Meta", (), {"unknown": EXCLUDE}),
62
+ }
63
+
64
+ for spec in request.specs:
65
+ namespace[spec.name] = _field_for(spec)
66
+
67
+ if request.nullable_names:
68
+ namespace["_coerce_null_strings"] = pre_load(_coercer(request))
69
+
70
+ return type(request.name, (Schema,), namespace)
71
+
72
+
73
+ def _field_for(spec: FilterSpec) -> fields.Field[Any]:
74
+ """Build the Marshmallow field for one filter."""
75
+
76
+ options: dict[str, Any] = {
77
+ "allow_none": spec.optional,
78
+ "load_default": spec.schema_default,
79
+ "metadata": {"description": spec.doc},
80
+ **spec.options,
81
+ }
82
+
83
+ return _field_for_type(spec.schema_annotation, options)
84
+
85
+
86
+ def _field_for_type(annotation: Any, options: dict[str, Any]) -> fields.Field[Any]:
87
+ """Map a Python annotation onto a concrete Marshmallow field."""
88
+
89
+ annotation, _ = unwrap_optional(annotation)
90
+
91
+ if (scalar := _SCALARS.get(annotation)) is not None:
92
+ return scalar(**options)
93
+
94
+ if isinstance(annotation, type) and issubclass(annotation, enum.Enum):
95
+ # Enums that carry a scalar value round-trip by value; the rest by name.
96
+ by_value = issubclass(annotation, (str, int))
97
+ return fields.Enum(annotation, by_value=by_value, **options)
98
+
99
+ origin = get_origin(annotation)
100
+
101
+ if origin in _SEQUENCES:
102
+ args = [arg for arg in get_args(annotation) if arg is not Ellipsis]
103
+ inner = _field_for_type(args[0], {}) if args else fields.Raw()
104
+ return fields.List(inner, **options)
105
+
106
+ if origin is Literal:
107
+ options.setdefault("validate", validate.OneOf(get_args(annotation)))
108
+ return fields.Raw(**options)
109
+
110
+ if origin is dict:
111
+ return fields.Dict(**options)
112
+
113
+ # Unknown annotation: pass the value through untouched rather than guessing.
114
+ return fields.Raw(**options)
115
+
116
+
117
+ def _coercer(request: SchemaRequest) -> Any:
118
+ """A ``pre_load`` hook turning null-like strings into ``None``."""
119
+
120
+ nullable = frozenset(request.nullable_names)
121
+
122
+ def coerce_null_strings(_: Any, data: Mapping[str, Any], **__: Any) -> dict[str, Any]:
123
+ return {
124
+ key: None if key in nullable and request.is_null_string(value) else value
125
+ for key, value in data.items()
126
+ }
127
+
128
+ return coerce_null_strings
@@ -0,0 +1,64 @@
1
+ """The Pydantic backend: a ``BaseModel`` subclass, validation included.
2
+
3
+ ``@options`` keywords go straight to :func:`pydantic.Field`, so constraints such as
4
+ ``min_length`` and ``ge`` work as they would on a handwritten model.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from pydantic import BaseModel, Field, create_model, field_validator
12
+
13
+ from .base import SchemaBackend
14
+
15
+ if TYPE_CHECKING:
16
+ from .base import SchemaRequest
17
+
18
+ __all__ = ("PydanticBackend",)
19
+
20
+
21
+ class PydanticBackend(SchemaBackend):
22
+ """Renders filters as a Pydantic model."""
23
+
24
+ name = "pydantic"
25
+ extra = "pydantic"
26
+
27
+ def build(self, request: SchemaRequest) -> type[BaseModel]:
28
+ fields: dict[str, Any] = {}
29
+
30
+ for spec in request.specs:
31
+ options = dict(spec.options)
32
+ options.setdefault("description", spec.doc)
33
+ fields[spec.name] = (
34
+ spec.schema_annotation,
35
+ Field(default=spec.schema_default, **options),
36
+ )
37
+
38
+ validators: dict[str, Any] = {}
39
+
40
+ if names := request.nullable_names:
41
+ # `mode="before"` so the empty string is caught ahead of type coercion.
42
+ validators["_coerce_null_strings"] = field_validator(*names, mode="before")(
43
+ _coercer(request)
44
+ )
45
+
46
+ model: type[BaseModel] = create_model(
47
+ request.name,
48
+ __module__=request.module,
49
+ __validators__=validators,
50
+ **fields,
51
+ )
52
+ # Assigned rather than passed as `__doc__=`, which create_model only grew in 2.1.
53
+ model.__doc__ = request.doc
54
+
55
+ return model
56
+
57
+
58
+ def _coercer(request: SchemaRequest) -> Any:
59
+ """A ``mode="before"`` validator turning null-like strings into ``None``."""
60
+
61
+ def coerce_null_strings(_: Any, value: Any) -> Any:
62
+ return None if request.is_null_string(value) else value
63
+
64
+ return classmethod(coerce_null_strings)
@@ -0,0 +1,55 @@
1
+ """Decorators applied to filter methods.
2
+
3
+ Each decorator only records intent on the function object. Nothing is validated or
4
+ built here; that happens once, lazily, when a schema is first requested.
5
+
6
+ ``options`` is deliberately untyped at runtime: the keywords it accepts depend on the
7
+ schema backend in use, and each backend namespace ships a stub that pins them down.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Any, TypeVar
13
+
14
+ from ._spec import OPTIONS_ATTR, SKIP_NULL_ATTR
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Callable
18
+
19
+ __all__ = ("options", "skip_null")
20
+
21
+ _FuncT = TypeVar("_FuncT", bound="Callable[..., Any]")
22
+
23
+
24
+ def options(**kwargs: Any) -> Callable[[_FuncT], _FuncT]:
25
+ """Attach schema field options to a filter.
26
+
27
+ The keywords are handed to the active backend verbatim, so they are whatever that
28
+ backend's field constructor takes. Import ``options`` from the same namespace as
29
+ ``Filters`` and your editor will hint the right ones.
30
+ """
31
+
32
+ def decorator(func: _FuncT) -> _FuncT:
33
+ existing = dict(getattr(func, OPTIONS_ATTR, {}))
34
+ existing.update(kwargs)
35
+ setattr(func, OPTIONS_ATTR, existing)
36
+
37
+ return func
38
+
39
+ return decorator
40
+
41
+
42
+ def skip_null(func: _FuncT) -> _FuncT:
43
+ """Let an explicit ``null`` switch off a filter that has a default.
44
+
45
+ Without it, a filter declaring ``value: Status = Status.ACTIVE`` always applies.
46
+ With it, the schema field accepts ``None`` -- and the strings ``""``, ``"null"``
47
+ and ``"none"``, so a query string can say it too -- and the filter is skipped.
48
+
49
+ It does nothing on a filter without a default, which is already skipped when its
50
+ value is ``None``.
51
+ """
52
+
53
+ setattr(func, SKIP_NULL_ATTR, True)
54
+
55
+ return func
@@ -0,0 +1,43 @@
1
+ """Exception and warning types raised by the library."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = (
6
+ "BackendNotAvailableError",
7
+ "FilterDeclarationError",
8
+ "FilterError",
9
+ "JoinConflictWarning",
10
+ "UnknownFilterError",
11
+ )
12
+
13
+
14
+ class FilterError(Exception):
15
+ """Base class for every error raised by this library."""
16
+
17
+
18
+ class FilterDeclarationError(FilterError, TypeError):
19
+ """A filter method is declared incorrectly.
20
+
21
+ Raised while the schema is being built, not while it is being applied.
22
+ """
23
+
24
+
25
+ class UnknownFilterError(FilterError, KeyError):
26
+ """A value was supplied for a name that is not a filter on the class."""
27
+
28
+ def __str__(self) -> str:
29
+ # KeyError.__str__ wraps the message in quotes; undo that.
30
+ return str(self.args[0]) if self.args else ""
31
+
32
+
33
+ class BackendNotAvailableError(FilterError, ImportError):
34
+ """A schema backend was requested but its optional dependency is missing."""
35
+
36
+
37
+ class JoinConflictWarning(UserWarning):
38
+ """A join was requested onto a target that is already joined another way.
39
+
40
+ The join already in place wins, whether an earlier filter added it or the caller
41
+ did before handing the statement over. Make the ``self.join(...)`` calls agree, or
42
+ alias the target so the two joins address distinct selectables.
43
+ """