pyrsql 0.0.1__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.
- pyrsql/__init__.py +63 -0
- pyrsql/adapters/__init__.py +3 -0
- pyrsql/adapters/fastapi/__init__.py +15 -0
- pyrsql/adapters/fastapi/config.py +76 -0
- pyrsql/adapters/fastapi/criteria.py +56 -0
- pyrsql/adapters/fastapi/dependency.py +217 -0
- pyrsql/adapters/fastapi/errors.py +107 -0
- pyrsql/api.py +76 -0
- pyrsql/core/__init__.py +45 -0
- pyrsql/core/compiler.py +81 -0
- pyrsql/core/conversion.py +206 -0
- pyrsql/core/custom.py +14 -0
- pyrsql/core/field_policy.py +97 -0
- pyrsql/core/joins.py +11 -0
- pyrsql/core/json/__init__.py +19 -0
- pyrsql/core/json/options.py +31 -0
- pyrsql/core/json/path.py +35 -0
- pyrsql/core/json/query.py +39 -0
- pyrsql/core/json/values.py +72 -0
- pyrsql/core/options.py +416 -0
- pyrsql/core/page.py +115 -0
- pyrsql/core/procedure_policy.py +61 -0
- pyrsql/core/query.py +160 -0
- pyrsql/core/sort.py +153 -0
- pyrsql/integrations/__init__.py +8 -0
- pyrsql/integrations/fastapi/__init__.py +8 -0
- pyrsql/integrations/fastapi/sqlalchemy.py +251 -0
- pyrsql/orms/__init__.py +15 -0
- pyrsql/orms/base.py +99 -0
- pyrsql/orms/sqlalchemy/__init__.py +39 -0
- pyrsql/orms/sqlalchemy/coercion.py +57 -0
- pyrsql/orms/sqlalchemy/compiled.py +56 -0
- pyrsql/orms/sqlalchemy/compiled_page.py +40 -0
- pyrsql/orms/sqlalchemy/compiled_sort.py +56 -0
- pyrsql/orms/sqlalchemy/custom.py +25 -0
- pyrsql/orms/sqlalchemy/errors.py +17 -0
- pyrsql/orms/sqlalchemy/introspection.py +107 -0
- pyrsql/orms/sqlalchemy/json_path.py +351 -0
- pyrsql/orms/sqlalchemy/orm.py +65 -0
- pyrsql/orms/sqlalchemy/resolver.py +202 -0
- pyrsql/orms/sqlalchemy/sorter.py +173 -0
- pyrsql/orms/sqlalchemy/statement.py +40 -0
- pyrsql/orms/sqlalchemy/translator.py +560 -0
- pyrsql/orms/sqlalchemy/type_inference.py +27 -0
- pyrsql/orms/sqlalchemy/types.py +64 -0
- pyrsql/parsing/__init__.py +32 -0
- pyrsql/parsing/ast.py +61 -0
- pyrsql/parsing/errors.py +25 -0
- pyrsql/parsing/lexer.py +264 -0
- pyrsql/parsing/limits.py +15 -0
- pyrsql/parsing/operators.py +201 -0
- pyrsql/parsing/parser.py +292 -0
- pyrsql/parsing/source.py +48 -0
- pyrsql/parsing/tokens.py +30 -0
- pyrsql/py.typed +0 -0
- pyrsql/selector/__init__.py +30 -0
- pyrsql/selector/analyzer.py +58 -0
- pyrsql/selector/ast.py +31 -0
- pyrsql/selector/parser.py +130 -0
- pyrsql/selector/semantic.py +34 -0
- pyrsql/semantic/__init__.py +23 -0
- pyrsql/semantic/analyzer.py +116 -0
- pyrsql/semantic/ast.py +35 -0
- pyrsql/semantic/errors.py +40 -0
- pyrsql/sorting/__init__.py +26 -0
- pyrsql/sorting/analyzer.py +74 -0
- pyrsql/sorting/ast.py +46 -0
- pyrsql/sorting/errors.py +21 -0
- pyrsql/sorting/limits.py +15 -0
- pyrsql/sorting/parser.py +150 -0
- pyrsql/sorting/semantic.py +15 -0
- pyrsql-0.0.1.dist-info/METADATA +72 -0
- pyrsql-0.0.1.dist-info/RECORD +74 -0
- pyrsql-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""SQLAlchemy ORM exports."""
|
|
2
|
+
|
|
3
|
+
from pyrsql.orms.sqlalchemy.coercion import SQLAlchemyValueCoercer
|
|
4
|
+
from pyrsql.orms.sqlalchemy.compiled import SQLAlchemyCompiledQuery
|
|
5
|
+
from pyrsql.orms.sqlalchemy.compiled_page import (
|
|
6
|
+
SQLAlchemyCompiledPageRequest,
|
|
7
|
+
)
|
|
8
|
+
from pyrsql.orms.sqlalchemy.compiled_sort import SQLAlchemyCompiledSort
|
|
9
|
+
from pyrsql.orms.sqlalchemy.errors import (
|
|
10
|
+
SQLAlchemyJSONSupportError,
|
|
11
|
+
SQLAlchemyModelInspectionError,
|
|
12
|
+
SQLAlchemyORMError,
|
|
13
|
+
SQLAlchemyPathResolutionError,
|
|
14
|
+
)
|
|
15
|
+
from pyrsql.orms.sqlalchemy.introspection import SQLAlchemyModelInspector
|
|
16
|
+
from pyrsql.orms.sqlalchemy.json_path import (
|
|
17
|
+
SQLAlchemyJSONPathExpressionBuilder,
|
|
18
|
+
)
|
|
19
|
+
from pyrsql.orms.sqlalchemy.orm import SQLAlchemyORM
|
|
20
|
+
from pyrsql.orms.sqlalchemy.resolver import SQLAlchemyPathResolver
|
|
21
|
+
from pyrsql.orms.sqlalchemy.sorter import SQLAlchemySortTranslator
|
|
22
|
+
from pyrsql.orms.sqlalchemy.translator import SQLAlchemyExpressionTranslator
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"SQLAlchemyORM",
|
|
26
|
+
"SQLAlchemyORMError",
|
|
27
|
+
"SQLAlchemyCompiledPageRequest",
|
|
28
|
+
"SQLAlchemyCompiledQuery",
|
|
29
|
+
"SQLAlchemyCompiledSort",
|
|
30
|
+
"SQLAlchemyJSONPathExpressionBuilder",
|
|
31
|
+
"SQLAlchemyJSONSupportError",
|
|
32
|
+
"SQLAlchemyModelInspector",
|
|
33
|
+
"SQLAlchemyModelInspectionError",
|
|
34
|
+
"SQLAlchemyPathResolutionError",
|
|
35
|
+
"SQLAlchemyPathResolver",
|
|
36
|
+
"SQLAlchemySortTranslator",
|
|
37
|
+
"SQLAlchemyExpressionTranslator",
|
|
38
|
+
"SQLAlchemyValueCoercer",
|
|
39
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""SQLAlchemy ORM value coercion."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pyrsql.core.conversion import (
|
|
6
|
+
FieldValueConverterSet,
|
|
7
|
+
ValueConversionError,
|
|
8
|
+
ValueConverterRegistry,
|
|
9
|
+
)
|
|
10
|
+
from pyrsql.orms.sqlalchemy.errors import SQLAlchemyORMError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SQLAlchemyValueCoercer:
|
|
14
|
+
"""Coerces semantic argument values using resolved Python types."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
registry: ValueConverterRegistry | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
self._registry = registry
|
|
22
|
+
|
|
23
|
+
def coerce(
|
|
24
|
+
self,
|
|
25
|
+
raw_value: str,
|
|
26
|
+
python_type: type[Any] | None,
|
|
27
|
+
*,
|
|
28
|
+
field_converter_set: FieldValueConverterSet | None = None,
|
|
29
|
+
model: type[Any] | None = None,
|
|
30
|
+
field_name: str | None = None,
|
|
31
|
+
field_path: str | None = None,
|
|
32
|
+
registry: ValueConverterRegistry | None = None,
|
|
33
|
+
) -> Any:
|
|
34
|
+
"""Coerces a raw argument into a typed Python value."""
|
|
35
|
+
if field_converter_set is not None:
|
|
36
|
+
field_converter = field_converter_set.resolve(
|
|
37
|
+
model=model,
|
|
38
|
+
field_name=field_name,
|
|
39
|
+
field_path=field_path,
|
|
40
|
+
)
|
|
41
|
+
if field_converter is not None:
|
|
42
|
+
try:
|
|
43
|
+
return field_converter(raw_value)
|
|
44
|
+
except Exception as error:
|
|
45
|
+
raise SQLAlchemyORMError(
|
|
46
|
+
f"Failed to convert {raw_value!r} for field "
|
|
47
|
+
f"{field_path or field_name!r}."
|
|
48
|
+
) from error
|
|
49
|
+
active_registry = registry or self._registry
|
|
50
|
+
if active_registry is None:
|
|
51
|
+
raise SQLAlchemyORMError(
|
|
52
|
+
"SQLAlchemyValueCoercer requires a conversion registry."
|
|
53
|
+
)
|
|
54
|
+
try:
|
|
55
|
+
return active_registry.convert(raw_value, python_type)
|
|
56
|
+
except ValueConversionError as error:
|
|
57
|
+
raise SQLAlchemyORMError(str(error)) from error
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Compiled query support for SQLAlchemy."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from pyrsql.core.options import QueryOptions
|
|
6
|
+
from pyrsql.orms.sqlalchemy.statement import (
|
|
7
|
+
apply_relationship_joins,
|
|
8
|
+
require_sqlalchemy_select,
|
|
9
|
+
)
|
|
10
|
+
from pyrsql.orms.sqlalchemy.translator import SQLAlchemyExpressionTranslator
|
|
11
|
+
from pyrsql.orms.sqlalchemy.types import SQLAlchemyModel, SQLAlchemySelect
|
|
12
|
+
from pyrsql.semantic.ast import SemanticExpression
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class SQLAlchemyCompiledQuery:
|
|
17
|
+
"""Compiled SQLAlchemy query plan.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
expression: Semantic expression to translate into SQLAlchemy clauses.
|
|
21
|
+
options: Query configuration used during translation.
|
|
22
|
+
translator: Translator responsible for producing SQLAlchemy objects.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
expression: SemanticExpression
|
|
26
|
+
options: QueryOptions
|
|
27
|
+
translator: SQLAlchemyExpressionTranslator
|
|
28
|
+
|
|
29
|
+
def apply(
|
|
30
|
+
self,
|
|
31
|
+
target: SQLAlchemySelect | object,
|
|
32
|
+
model: SQLAlchemyModel,
|
|
33
|
+
) -> SQLAlchemySelect:
|
|
34
|
+
"""Applies the compiled query to a SQLAlchemy Select.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
target: SQLAlchemy select statement to mutate.
|
|
38
|
+
model: SQLAlchemy mapped class used to resolve fields.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A SQLAlchemy select with filters and joins applied.
|
|
42
|
+
"""
|
|
43
|
+
statement = require_sqlalchemy_select(target)
|
|
44
|
+
joins, predicate = self.translator.translate(
|
|
45
|
+
model,
|
|
46
|
+
self.expression,
|
|
47
|
+
options=self.options,
|
|
48
|
+
)
|
|
49
|
+
statement = apply_relationship_joins(
|
|
50
|
+
statement,
|
|
51
|
+
joins,
|
|
52
|
+
join_hints=self.options.join_hints,
|
|
53
|
+
)
|
|
54
|
+
if self.options.distinct:
|
|
55
|
+
statement = statement.distinct()
|
|
56
|
+
return statement.where(predicate)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Compiled pagination support for SQLAlchemy."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from pyrsql.core.page import PageRequest
|
|
6
|
+
from pyrsql.orms.sqlalchemy.statement import require_sqlalchemy_select
|
|
7
|
+
from pyrsql.orms.sqlalchemy.types import SQLAlchemyModel, SQLAlchemySelect
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class SQLAlchemyCompiledPageRequest:
|
|
12
|
+
"""Compiled SQLAlchemy pagination plan.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
page_request: Pagination request to apply to a select statement.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
page_request: PageRequest
|
|
19
|
+
|
|
20
|
+
def apply(
|
|
21
|
+
self,
|
|
22
|
+
target: SQLAlchemySelect | object,
|
|
23
|
+
model: SQLAlchemyModel,
|
|
24
|
+
) -> SQLAlchemySelect:
|
|
25
|
+
"""Applies the compiled page request to a SQLAlchemy Select.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
target: SQLAlchemy select statement to mutate.
|
|
29
|
+
model: SQLAlchemy mapped class. The page application does not use
|
|
30
|
+
it directly, but the signature matches the other compiled
|
|
31
|
+
plans.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A SQLAlchemy select with limit and offset applied.
|
|
35
|
+
"""
|
|
36
|
+
del model
|
|
37
|
+
statement = require_sqlalchemy_select(target)
|
|
38
|
+
return statement.limit(self.page_request.limit).offset(
|
|
39
|
+
self.page_request.offset
|
|
40
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Compiled sort support for SQLAlchemy."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from pyrsql.core.options import SortOptions
|
|
6
|
+
from pyrsql.orms.sqlalchemy.sorter import SQLAlchemySortTranslator
|
|
7
|
+
from pyrsql.orms.sqlalchemy.statement import (
|
|
8
|
+
apply_relationship_joins,
|
|
9
|
+
require_sqlalchemy_select,
|
|
10
|
+
)
|
|
11
|
+
from pyrsql.orms.sqlalchemy.types import SQLAlchemyModel, SQLAlchemySelect
|
|
12
|
+
from pyrsql.sorting.semantic import SemanticSortField
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class SQLAlchemyCompiledSort:
|
|
17
|
+
"""Compiled SQLAlchemy sort plan.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
fields: Semantic sort fields to translate into order clauses.
|
|
21
|
+
options: Sort configuration used during translation.
|
|
22
|
+
translator: Translator responsible for producing SQLAlchemy objects.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
fields: tuple[SemanticSortField, ...]
|
|
26
|
+
options: SortOptions
|
|
27
|
+
translator: SQLAlchemySortTranslator
|
|
28
|
+
|
|
29
|
+
def apply(
|
|
30
|
+
self,
|
|
31
|
+
target: SQLAlchemySelect | object,
|
|
32
|
+
model: SQLAlchemyModel,
|
|
33
|
+
) -> SQLAlchemySelect:
|
|
34
|
+
"""Applies the compiled sort to a SQLAlchemy Select.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
target: SQLAlchemy select statement to mutate.
|
|
38
|
+
model: SQLAlchemy mapped class used to resolve fields.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A SQLAlchemy select with joins and ordering applied.
|
|
42
|
+
"""
|
|
43
|
+
statement = require_sqlalchemy_select(target)
|
|
44
|
+
joins, order_clauses = self.translator.translate(
|
|
45
|
+
model,
|
|
46
|
+
self.fields,
|
|
47
|
+
options=self.options,
|
|
48
|
+
)
|
|
49
|
+
statement = apply_relationship_joins(
|
|
50
|
+
statement,
|
|
51
|
+
joins,
|
|
52
|
+
join_hints=self.options.join_hints,
|
|
53
|
+
)
|
|
54
|
+
if not order_clauses:
|
|
55
|
+
return statement
|
|
56
|
+
return statement.order_by(*order_clauses)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Custom predicate extension points for the SQLAlchemy orm."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
8
|
+
|
|
9
|
+
from pyrsql.core.options import QueryOptions
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class SQLAlchemyCustomPredicateInput:
|
|
14
|
+
"""Structured input for ORM-specific custom predicate builders."""
|
|
15
|
+
|
|
16
|
+
expression: ColumnElement[Any]
|
|
17
|
+
python_type: type[Any] | None
|
|
18
|
+
values: tuple[Any, ...]
|
|
19
|
+
options: QueryOptions
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SQLAlchemyCustomPredicate = Callable[
|
|
23
|
+
[SQLAlchemyCustomPredicateInput],
|
|
24
|
+
ColumnElement[bool],
|
|
25
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""SQLAlchemy ORM-specific exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SQLAlchemyORMError(ValueError):
|
|
5
|
+
"""Base exception for SQLAlchemy ORM failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SQLAlchemyModelInspectionError(SQLAlchemyORMError):
|
|
9
|
+
"""Raised when a model cannot be inspected as a mapped class."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SQLAlchemyPathResolutionError(SQLAlchemyORMError):
|
|
13
|
+
"""Raised when a semantic field path cannot be resolved."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SQLAlchemyJSONSupportError(SQLAlchemyORMError):
|
|
17
|
+
"""Raised when JSON/JSONB translation cannot be completed."""
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""SQLAlchemy ORM inspection helpers."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import inspect
|
|
6
|
+
from sqlalchemy.exc import NoInspectionAvailable
|
|
7
|
+
from sqlalchemy.orm import Mapper
|
|
8
|
+
from sqlalchemy.orm.properties import ColumnProperty
|
|
9
|
+
from sqlalchemy.sql.sqltypes import JSON
|
|
10
|
+
|
|
11
|
+
from pyrsql.orms.sqlalchemy.errors import SQLAlchemyModelInspectionError
|
|
12
|
+
from pyrsql.orms.sqlalchemy.types import (
|
|
13
|
+
SQLAlchemyAttributeKind,
|
|
14
|
+
SQLAlchemyMappedAttribute,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SQLAlchemyModelInspector:
|
|
19
|
+
"""Provides stable access to public SQLAlchemy ORM inspection APIs."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self._mapper_cache: dict[type[Any], Mapper[Any]] = {}
|
|
23
|
+
self._attribute_cache: dict[
|
|
24
|
+
tuple[type[Any], str], SQLAlchemyMappedAttribute
|
|
25
|
+
] = {}
|
|
26
|
+
|
|
27
|
+
def inspect_model(self, model: type[Any]) -> Mapper[Any]:
|
|
28
|
+
"""Returns the ORM mapper for a mapped class."""
|
|
29
|
+
cached_mapper = self._mapper_cache.get(model)
|
|
30
|
+
if cached_mapper is not None:
|
|
31
|
+
return cached_mapper
|
|
32
|
+
try:
|
|
33
|
+
mapper = inspect(model)
|
|
34
|
+
except NoInspectionAvailable as error:
|
|
35
|
+
raise SQLAlchemyModelInspectionError(
|
|
36
|
+
f"Type {model!r} is not a SQLAlchemy mapped class."
|
|
37
|
+
) from error
|
|
38
|
+
if not isinstance(mapper, Mapper):
|
|
39
|
+
raise SQLAlchemyModelInspectionError(
|
|
40
|
+
f"Type {model!r} did not resolve to a SQLAlchemy ORM mapper."
|
|
41
|
+
)
|
|
42
|
+
self._mapper_cache[model] = mapper
|
|
43
|
+
return mapper
|
|
44
|
+
|
|
45
|
+
def get_mapped_attribute(
|
|
46
|
+
self,
|
|
47
|
+
model: type[Any],
|
|
48
|
+
attribute_name: str,
|
|
49
|
+
) -> SQLAlchemyMappedAttribute:
|
|
50
|
+
"""Returns metadata for a mapped attribute by name."""
|
|
51
|
+
cache_key = (model, attribute_name)
|
|
52
|
+
cached_attribute = self._attribute_cache.get(cache_key)
|
|
53
|
+
if cached_attribute is not None:
|
|
54
|
+
return cached_attribute
|
|
55
|
+
|
|
56
|
+
mapper = self.inspect_model(model)
|
|
57
|
+
if attribute_name in mapper.relationships:
|
|
58
|
+
relationship = mapper.relationships[attribute_name]
|
|
59
|
+
mapped_attribute = SQLAlchemyMappedAttribute(
|
|
60
|
+
name=attribute_name,
|
|
61
|
+
kind=SQLAlchemyAttributeKind.RELATIONSHIP,
|
|
62
|
+
owner_model=model,
|
|
63
|
+
attribute=getattr(model, attribute_name),
|
|
64
|
+
mapper=relationship.mapper,
|
|
65
|
+
python_type=relationship.mapper.class_,
|
|
66
|
+
is_collection=bool(relationship.uselist),
|
|
67
|
+
)
|
|
68
|
+
self._attribute_cache[cache_key] = mapped_attribute
|
|
69
|
+
return mapped_attribute
|
|
70
|
+
if attribute_name in mapper.column_attrs:
|
|
71
|
+
column_property = mapper.column_attrs[attribute_name]
|
|
72
|
+
mapped_attribute = SQLAlchemyMappedAttribute(
|
|
73
|
+
name=attribute_name,
|
|
74
|
+
kind=SQLAlchemyAttributeKind.COLUMN,
|
|
75
|
+
owner_model=model,
|
|
76
|
+
attribute=getattr(model, attribute_name),
|
|
77
|
+
mapper=None,
|
|
78
|
+
python_type=self._resolve_column_python_type(column_property),
|
|
79
|
+
is_json=self._is_json_column(column_property),
|
|
80
|
+
)
|
|
81
|
+
self._attribute_cache[cache_key] = mapped_attribute
|
|
82
|
+
return mapped_attribute
|
|
83
|
+
raise SQLAlchemyModelInspectionError(
|
|
84
|
+
f"Attribute {attribute_name!r} is not mapped on model "
|
|
85
|
+
f"{model.__name__!r}."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _resolve_column_python_type(
|
|
90
|
+
column_property: ColumnProperty[Any],
|
|
91
|
+
) -> type[Any] | None:
|
|
92
|
+
"""Returns the Python type for a column property when available."""
|
|
93
|
+
if not column_property.columns:
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
return column_property.columns[0].type.python_type
|
|
97
|
+
except (AttributeError, NotImplementedError):
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _is_json_column(
|
|
102
|
+
column_property: ColumnProperty[Any],
|
|
103
|
+
) -> bool:
|
|
104
|
+
"""Returns whether the column property stores JSON-like data."""
|
|
105
|
+
if not column_property.columns:
|
|
106
|
+
return False
|
|
107
|
+
return isinstance(column_property.columns[0].type, JSON)
|