persistence-kit 3.0.0__tar.gz → 3.0.2__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.
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/PKG-INFO +1 -1
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/filter_ops.py +34 -3
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/mongo_repo/mongo_repo.py +53 -3
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_engine.py +5 -2
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_repo.py +25 -4
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/settings/repo_settings.py +2 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/pyproject.toml +1 -1
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/LICENSE +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/README.md +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/api/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/api/common.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/api/error_handlers.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/api/exceptions.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/api/route_loader.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/bootstrap/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/bootstrap/configuration.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/bootstrap/seeders.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/bootstrap/startup.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/contracts/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/contracts/repository.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/contracts/view_repository.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/py.typed +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/dynamodb_repo/dynamodb_mapper.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/dynamodb_repo/dynamodb_repo.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/memory_repo/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/memory_repo/memory_repo.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/mongo_repo/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/mongo_repo/mongo_mapper.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/sqlalchemy_repo/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/sqlalchemy_repo/schema_evolve.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_dataclass_mapper.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/sqlalchemy_repo/table_factory.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository_factory/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository_factory/factory/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository_factory/factory/repository_factory.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository_factory/registry/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository_factory/registry/entity_registry.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository_factory/view/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository_factory/view/populating_repository.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/settings/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/settings/constants.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/settings/parsers.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/utils/__init__.py +0 -0
- {persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/utils/upsert.py +0 -0
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
import unicodedata
|
|
3
5
|
from typing import Any, Callable, Iterable, Mapping
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
_SIMPLE_OPS = {"gte", "gt", "lte", "lt", "eq", "ne"}
|
|
7
9
|
_STRING_OPS = {"contains", "icontains", "startswith", "istartswith", "endswith", "iendswith"}
|
|
8
10
|
_LOGICAL_KEYS = {"or", "and"}
|
|
11
|
+
_NON_ALNUM_RE = re.compile(r"[^0-9a-z]+")
|
|
12
|
+
_WHITESPACE_RE = re.compile(r"\s+")
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
def is_multi_value(value: Any) -> bool:
|
|
@@ -49,6 +53,23 @@ def iter_range_ops(value: Mapping[str, Any]) -> Iterable[tuple[str, Any]]:
|
|
|
49
53
|
return items
|
|
50
54
|
|
|
51
55
|
|
|
56
|
+
def normalize_search_text(value: Any) -> str:
|
|
57
|
+
if value is None:
|
|
58
|
+
return ""
|
|
59
|
+
normalized = unicodedata.normalize("NFKD", str(value))
|
|
60
|
+
without_marks = "".join(ch for ch in normalized if not unicodedata.combining(ch))
|
|
61
|
+
lowered = without_marks.lower()
|
|
62
|
+
collapsed = _NON_ALNUM_RE.sub(" ", lowered)
|
|
63
|
+
return _WHITESPACE_RE.sub(" ", collapsed).strip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def tokenize_search_text(value: Any) -> list[str]:
|
|
67
|
+
normalized = normalize_search_text(value)
|
|
68
|
+
if not normalized:
|
|
69
|
+
return []
|
|
70
|
+
return normalized.split()
|
|
71
|
+
|
|
72
|
+
|
|
52
73
|
def _matches_string_op(current: Any, op: str, expected: Any) -> bool:
|
|
53
74
|
if current is None:
|
|
54
75
|
return False
|
|
@@ -57,15 +78,25 @@ def _matches_string_op(current: Any, op: str, expected: Any) -> bool:
|
|
|
57
78
|
if op == "contains":
|
|
58
79
|
return expected_text in current_text
|
|
59
80
|
if op == "icontains":
|
|
60
|
-
|
|
81
|
+
expected_tokens = tokenize_search_text(expected)
|
|
82
|
+
if not expected_tokens:
|
|
83
|
+
return False
|
|
84
|
+
normalized_current = normalize_search_text(current)
|
|
85
|
+
return all(token in normalized_current for token in expected_tokens)
|
|
61
86
|
if op == "startswith":
|
|
62
87
|
return current_text.startswith(expected_text)
|
|
63
88
|
if op == "istartswith":
|
|
64
|
-
|
|
89
|
+
normalized_expected = normalize_search_text(expected)
|
|
90
|
+
if not normalized_expected:
|
|
91
|
+
return False
|
|
92
|
+
return normalize_search_text(current).startswith(normalized_expected)
|
|
65
93
|
if op == "endswith":
|
|
66
94
|
return current_text.endswith(expected_text)
|
|
67
95
|
if op == "iendswith":
|
|
68
|
-
|
|
96
|
+
normalized_expected = normalize_search_text(expected)
|
|
97
|
+
if not normalized_expected:
|
|
98
|
+
return False
|
|
99
|
+
return normalize_search_text(current).endswith(normalized_expected)
|
|
69
100
|
raise ValueError(f"Unsupported operator: {op}")
|
|
70
101
|
|
|
71
102
|
|
{persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/mongo_repo/mongo_repo.py
RENAMED
|
@@ -16,10 +16,21 @@ from persistence_kit.repository.filter_ops import (
|
|
|
16
16
|
is_multi_value,
|
|
17
17
|
is_range_dict,
|
|
18
18
|
iter_range_ops,
|
|
19
|
+
normalize_search_text,
|
|
20
|
+
tokenize_search_text,
|
|
19
21
|
)
|
|
20
22
|
|
|
21
23
|
T = TypeVar("T")
|
|
22
24
|
TId = TypeVar("TId", bound=Hashable)
|
|
25
|
+
_ACCENT_CLASSES = {
|
|
26
|
+
"a": "[aáàäâã]",
|
|
27
|
+
"e": "[eéèëê]",
|
|
28
|
+
"i": "[iíìïî]",
|
|
29
|
+
"o": "[oóòöôõ]",
|
|
30
|
+
"u": "[uúùüû]",
|
|
31
|
+
"n": "[nñ]",
|
|
32
|
+
"c": "[cç]",
|
|
33
|
+
}
|
|
23
34
|
|
|
24
35
|
class EntityMapper(Protocol[T, TId]):
|
|
25
36
|
def collection(self) -> str: ...
|
|
@@ -34,6 +45,36 @@ class EntityMapper(Protocol[T, TId]):
|
|
|
34
45
|
def _normalize_field(mapper: EntityMapper[T, TId], field: str) -> str:
|
|
35
46
|
return mapper.attr_to_storage(field)
|
|
36
47
|
|
|
48
|
+
|
|
49
|
+
def _token_to_regex(token: str) -> str:
|
|
50
|
+
parts: list[str] = []
|
|
51
|
+
for ch in token:
|
|
52
|
+
parts.append(_ACCENT_CLASSES.get(ch, re.escape(ch)))
|
|
53
|
+
return "".join(parts)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _build_icontains_pattern(value: Any) -> str | None:
|
|
57
|
+
terms = tokenize_search_text(value)
|
|
58
|
+
if not terms:
|
|
59
|
+
return None
|
|
60
|
+
return "".join(f"(?=.*{_token_to_regex(term)})" for term in terms)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _build_ordered_pattern(value: Any, *, anchor_start: bool = False, anchor_end: bool = False) -> str | None:
|
|
64
|
+
normalized = normalize_search_text(value)
|
|
65
|
+
if not normalized:
|
|
66
|
+
return None
|
|
67
|
+
terms = tokenize_search_text(normalized)
|
|
68
|
+
if not terms:
|
|
69
|
+
return None
|
|
70
|
+
core = ".*".join(_token_to_regex(term) for term in terms)
|
|
71
|
+
if anchor_start:
|
|
72
|
+
core = f"^{core}"
|
|
73
|
+
if anchor_end:
|
|
74
|
+
core = f"{core}$"
|
|
75
|
+
return core
|
|
76
|
+
|
|
77
|
+
|
|
37
78
|
def _range_to_mongo(value: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
38
79
|
query: dict[str, Any] = {}
|
|
39
80
|
for op, v in iter_range_ops(value):
|
|
@@ -57,17 +98,26 @@ def _range_to_mongo(value: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
|
57
98
|
elif op == "contains":
|
|
58
99
|
query["$regex"] = re.escape(str(v))
|
|
59
100
|
elif op == "icontains":
|
|
60
|
-
|
|
101
|
+
pattern = _build_icontains_pattern(v)
|
|
102
|
+
if pattern is None:
|
|
103
|
+
continue
|
|
104
|
+
query["$regex"] = pattern
|
|
61
105
|
query["$options"] = "i"
|
|
62
106
|
elif op == "startswith":
|
|
63
107
|
query["$regex"] = f"^{re.escape(str(v))}"
|
|
64
108
|
elif op == "istartswith":
|
|
65
|
-
|
|
109
|
+
pattern = _build_ordered_pattern(v, anchor_start=True)
|
|
110
|
+
if pattern is None:
|
|
111
|
+
continue
|
|
112
|
+
query["$regex"] = pattern
|
|
66
113
|
query["$options"] = "i"
|
|
67
114
|
elif op == "endswith":
|
|
68
115
|
query["$regex"] = f"{re.escape(str(v))}$"
|
|
69
116
|
elif op == "iendswith":
|
|
70
|
-
|
|
117
|
+
pattern = _build_ordered_pattern(v, anchor_end=True)
|
|
118
|
+
if pattern is None:
|
|
119
|
+
continue
|
|
120
|
+
query["$regex"] = pattern
|
|
71
121
|
query["$options"] = "i"
|
|
72
122
|
return query
|
|
73
123
|
|
|
@@ -5,7 +5,10 @@ from persistence_kit.settings.repo_settings import RepoSettings
|
|
|
5
5
|
@lru_cache
|
|
6
6
|
def get_engine() -> AsyncEngine:
|
|
7
7
|
s = RepoSettings()
|
|
8
|
-
url =
|
|
8
|
+
url = s.postgres_dsn
|
|
9
9
|
if not url:
|
|
10
10
|
url = f"postgresql+asyncpg://{s.postgres_user}:{s.postgres_password}@{s.postgres_host}:{s.postgres_port}/{s.postgres_db}"
|
|
11
|
-
|
|
11
|
+
kwargs: dict = {"future": True}
|
|
12
|
+
if s.postgres_ssl:
|
|
13
|
+
kwargs["connect_args"] = {"ssl": "require"}
|
|
14
|
+
return create_async_engine(url, **kwargs)
|
|
@@ -6,7 +6,7 @@ try:
|
|
|
6
6
|
except ImportError:
|
|
7
7
|
from typing_extensions import override
|
|
8
8
|
|
|
9
|
-
from sqlalchemy import Table, select, insert, update as sql_update, delete as sql_delete, Index, and_, or_, func, distinct
|
|
9
|
+
from sqlalchemy import Table, select, insert, update as sql_update, delete as sql_delete, Index, and_, or_, func, distinct, String, cast
|
|
10
10
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
11
11
|
|
|
12
12
|
from persistence_kit.contracts.repository import Repository
|
|
@@ -16,6 +16,8 @@ from persistence_kit.repository.filter_ops import (
|
|
|
16
16
|
is_multi_value,
|
|
17
17
|
is_range_dict,
|
|
18
18
|
iter_range_ops,
|
|
19
|
+
normalize_search_text,
|
|
20
|
+
tokenize_search_text,
|
|
19
21
|
)
|
|
20
22
|
from persistence_kit.repository.sqlalchemy_repo.schema_evolve import (
|
|
21
23
|
ensure_missing_columns,
|
|
@@ -25,6 +27,8 @@ from persistence_kit.repository_factory.registry.entity_registry import build_fk
|
|
|
25
27
|
|
|
26
28
|
T = TypeVar("T")
|
|
27
29
|
TId = TypeVar("TId", bound=Hashable)
|
|
30
|
+
_ACCENTED_CHARS = "ÁÀÄÂÃáàäâãÉÈËÊéèëêÍÌÏÎíìïîÓÒÖÔÕóòöôõÚÙÜÛúùüûÑñÇç"
|
|
31
|
+
_PLAIN_CHARS = "AAAAAaaaaaEEEEeeeeIIIIiiiiOOOOOoooooUUUUuuuuNnCc"
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
class SqlAlchemyEntityMapper(Protocol[T, TId]):
|
|
@@ -39,6 +43,12 @@ class SqlAlchemyEntityMapper(Protocol[T, TId]):
|
|
|
39
43
|
def attr_to_storage(self, name: str) -> str: ...
|
|
40
44
|
|
|
41
45
|
|
|
46
|
+
def _normalized_text_expr(col: Any) -> Any:
|
|
47
|
+
text_col = cast(func.coalesce(col, ""), String)
|
|
48
|
+
lowered = func.lower(func.translate(text_col, _ACCENTED_CHARS, _PLAIN_CHARS))
|
|
49
|
+
return func.regexp_replace(lowered, r"[^a-z0-9]+", " ", "g")
|
|
50
|
+
|
|
51
|
+
|
|
42
52
|
def _build_where_clauses(
|
|
43
53
|
table: Table,
|
|
44
54
|
mapper: SqlAlchemyEntityMapper[T, TId],
|
|
@@ -104,15 +114,26 @@ def _build_where_clauses(
|
|
|
104
114
|
elif op == "contains":
|
|
105
115
|
clauses.append(col.contains(str(v)))
|
|
106
116
|
elif op == "icontains":
|
|
107
|
-
|
|
117
|
+
normalized_terms = tokenize_search_text(v)
|
|
118
|
+
if not normalized_terms:
|
|
119
|
+
return None
|
|
120
|
+
normalized_col = _normalized_text_expr(col)
|
|
121
|
+
for term in normalized_terms:
|
|
122
|
+
clauses.append(normalized_col.like(f"%{term}%"))
|
|
108
123
|
elif op == "startswith":
|
|
109
124
|
clauses.append(col.startswith(str(v)))
|
|
110
125
|
elif op == "istartswith":
|
|
111
|
-
|
|
126
|
+
normalized_value = normalize_search_text(v)
|
|
127
|
+
if not normalized_value:
|
|
128
|
+
return None
|
|
129
|
+
clauses.append(_normalized_text_expr(col).like(f"{normalized_value}%"))
|
|
112
130
|
elif op == "endswith":
|
|
113
131
|
clauses.append(col.endswith(str(v)))
|
|
114
132
|
elif op == "iendswith":
|
|
115
|
-
|
|
133
|
+
normalized_value = normalize_search_text(v)
|
|
134
|
+
if not normalized_value:
|
|
135
|
+
return None
|
|
136
|
+
clauses.append(_normalized_text_expr(col).like(f"%{normalized_value}"))
|
|
116
137
|
else:
|
|
117
138
|
clauses.append(col == value)
|
|
118
139
|
return clauses
|
|
@@ -12,11 +12,13 @@ class RepoSettings(BaseSettings):
|
|
|
12
12
|
mongo_dsn: str | None = None
|
|
13
13
|
mongo_db: str | None = None
|
|
14
14
|
|
|
15
|
+
postgres_dsn: str | None = None
|
|
15
16
|
postgres_user: str | None = None
|
|
16
17
|
postgres_password: str | None = None
|
|
17
18
|
postgres_host: str | None = None
|
|
18
19
|
postgres_port: int | None = 5432
|
|
19
20
|
postgres_db: str | None = None
|
|
21
|
+
postgres_ssl: bool = False
|
|
20
22
|
|
|
21
23
|
dynamodb_table_prefix: str = ""
|
|
22
24
|
dynamodb_region: str = "us-east-1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/contracts/view_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/memory_repo/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository/mongo_repo/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository_factory/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.0.0 → persistence_kit-3.0.2}/persistence_kit/repository_factory/view/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|