fastapi-sqlalchemy-toolkit 0.8.2.4__tar.gz → 0.8.3.1__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.
Files changed (62) hide show
  1. fastapi_sqlalchemy_toolkit-0.8.3.1/.github/workflows/python-publish.yml +33 -0
  2. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/PKG-INFO +3 -2
  3. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/fastapi_sqlalchemy_toolkit/model_manager.py +32 -8
  4. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/pyproject.toml +2 -1
  5. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/requirements/base.txt +1 -0
  6. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/tests/test_partial_where.py +56 -0
  7. fastapi_sqlalchemy_toolkit-0.8.2.4/.github/workflows/python-publish.yml +0 -34
  8. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/.flake8 +0 -0
  9. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/.github/workflows/docker-test.yml +0 -0
  10. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/.gitignore +0 -0
  11. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/.pylintrc +0 -0
  12. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/AUTHORS +0 -0
  13. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/LICENSE +0 -0
  14. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/Makefile +0 -0
  15. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/README.md +0 -0
  16. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/benefits.md +0 -0
  17. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/db_validation.md +0 -0
  18. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/extension.md +0 -0
  19. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/filtering.md +0 -0
  20. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/index.md +0 -0
  21. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/ru/benefits.md +0 -0
  22. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/ru/db_validation.md +0 -0
  23. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/ru/extension.md +0 -0
  24. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/ru/filtering.md +0 -0
  25. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/ru/index.md +0 -0
  26. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/ru/sorting.md +0 -0
  27. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/ru/transactions.md +0 -0
  28. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/ru/usage.md +0 -0
  29. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/ru/utils.md +0 -0
  30. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/sorting.md +0 -0
  31. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/transactions.md +0 -0
  32. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/usage.md +0 -0
  33. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/docs/utils.md +0 -0
  34. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/.env +0 -0
  35. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/__init__.py +0 -0
  36. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/api/__init__.py +0 -0
  37. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/api/api.py +0 -0
  38. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/api/deps.py +0 -0
  39. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/api/endpoints/__init__.py +0 -0
  40. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/api/endpoints/child.py +0 -0
  41. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/config.py +0 -0
  42. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/db.py +0 -0
  43. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/main.py +0 -0
  44. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/managers.py +0 -0
  45. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/models.py +0 -0
  46. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/examples/app/schemas.py +0 -0
  47. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/fastapi_sqlalchemy_toolkit/__init__.py +0 -0
  48. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/fastapi_sqlalchemy_toolkit/filters.py +0 -0
  49. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/fastapi_sqlalchemy_toolkit/ordering.py +0 -0
  50. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/fastapi_sqlalchemy_toolkit/utils.py +0 -0
  51. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/mkdocs.yml +0 -0
  52. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/poetry.lock +0 -0
  53. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/requirements/docs.txt +0 -0
  54. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/requirements/lint.txt +0 -0
  55. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/requirements/test.txt +0 -0
  56. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/tests/Dockerfile +0 -0
  57. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/tests/__init__.py +0 -0
  58. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/tests/conftest.py +0 -0
  59. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/tests/db.py +0 -0
  60. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/tests/docker-compose.yml +0 -0
  61. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/tests/models.py +0 -0
  62. {fastapi_sqlalchemy_toolkit-0.8.2.4 → fastapi_sqlalchemy_toolkit-0.8.3.1}/tests/test_public_methods.py +0 -0
@@ -0,0 +1,33 @@
1
+ # This workflow will upload a Python Package using Twine when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3
+
4
+ name: Upload Python Package
5
+
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ deploy:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - uses: actions/checkout@v3
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v3
21
+ with:
22
+ python-version: "3.x"
23
+ - name: Install dependencies
24
+ run: |
25
+ python -m pip install --upgrade pip
26
+ pip install build
27
+ - name: Build package
28
+ run: python -m build
29
+ - name: Publish package
30
+ uses: pypa/gh-action-pypi-publish@release/v1
31
+ with:
32
+ user: __token__
33
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: fastapi_sqlalchemy_toolkit
3
- Version: 0.8.2.4
3
+ Version: 0.8.3.1
4
4
  Summary: FastAPI SQLAlchemy Toolkit
5
5
  Project-URL: Homepage, https://github.com/e-kondr01/fastapi-sqlalchemy-toolkit
6
6
  Author-email: Egor Kondrashov <e.kondr01@gmail.com>
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3
11
11
  Requires-Python: >=3.11
12
12
  Requires-Dist: fastapi-pagination>=0.12.12
13
13
  Requires-Dist: fastapi>=0.100.0
14
+ Requires-Dist: greenlet>=3.0.0
14
15
  Requires-Dist: pydantic>=2.0.0
15
16
  Requires-Dist: sqlalchemy>=2.0.0
16
17
  Description-Content-Type: text/markdown
@@ -1,4 +1,5 @@
1
1
  # ruff: noqa: UP006
2
+ import re
2
3
  from collections.abc import Callable, Iterable
3
4
  from enum import Enum
4
5
  from typing import Any, Generic, List, TypeVar, overload # noqa: UP035
@@ -1504,6 +1505,11 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1504
1505
  return True
1505
1506
  return str(actual_normalized) == str(expected_normalized)
1506
1507
 
1508
+ _NULL_CHECK_RE = re.compile(
1509
+ r"^\s*(?P<column>[A-Za-z_][A-Za-z0-9_]*)\s+IS\s+(?P<not>NOT\s+)?NULL\s*$",
1510
+ re.IGNORECASE,
1511
+ )
1512
+
1507
1513
  @classmethod
1508
1514
  def _matches_partial_index_where(cls, condition: Any, in_obj: ModelDict) -> bool:
1509
1515
  """
@@ -1520,6 +1526,14 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1520
1526
  column_name: str | None = None
1521
1527
  expected: Any = None
1522
1528
  if isinstance(condition, TextClause):
1529
+ null_match = cls._NULL_CHECK_RE.match(condition.text)
1530
+ if null_match:
1531
+ column_name = null_match.group("column")
1532
+ if column_name not in in_obj:
1533
+ return True
1534
+ is_null = in_obj[column_name] is None
1535
+ return not is_null if null_match.group("not") else is_null
1536
+
1523
1537
  raw_column, separator, raw_expected = condition.text.partition("=")
1524
1538
  column_name = raw_column.strip()
1525
1539
  raw_expected = raw_expected.strip()
@@ -1537,18 +1551,28 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1537
1551
  expected = raw_expected
1538
1552
  else:
1539
1553
  column_name = None
1540
- elif (
1541
- isinstance(condition, BinaryExpression)
1542
- and condition.operator is operators.eq
1554
+ elif isinstance(condition, BinaryExpression) and condition.operator in (
1555
+ operators.eq,
1556
+ operators.is_,
1557
+ operators.isnot,
1543
1558
  ):
1544
1559
  column_name = getattr(condition.left, "name", None) or getattr(
1545
1560
  condition.left, "key", None
1546
1561
  )
1547
- expected = getattr(
1548
- condition.right,
1549
- "value",
1550
- getattr(condition.right, "effective_value", condition.right),
1551
- )
1562
+ if condition.operator in (operators.is_, operators.isnot):
1563
+ if not isinstance(column_name, str) or not isinstance(
1564
+ condition.right, Null
1565
+ ):
1566
+ column_name = None
1567
+ elif column_name in in_obj:
1568
+ is_null = in_obj[column_name] is None
1569
+ return not is_null if condition.operator is operators.isnot else is_null
1570
+ else:
1571
+ expected = getattr(
1572
+ condition.right,
1573
+ "value",
1574
+ getattr(condition.right, "effective_value", condition.right),
1575
+ )
1552
1576
 
1553
1577
  if not isinstance(column_name, str) or column_name not in in_obj:
1554
1578
  return True
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fastapi_sqlalchemy_toolkit"
7
- version = "0.8.2.4"
7
+ version = "0.8.3.1"
8
8
  authors = [
9
9
  { name="Egor Kondrashov", email="e.kondr01@gmail.com" },
10
10
  ]
@@ -18,6 +18,7 @@ classifiers = [
18
18
  dependencies = [
19
19
  "fastapi>=0.100.0",
20
20
  "sqlalchemy>=2.0.0",
21
+ "greenlet>=3.0.0",
21
22
  "fastapi_pagination>=0.12.12",
22
23
  "pydantic>=2.0.0",
23
24
  ]
@@ -1,4 +1,5 @@
1
1
  fastapi>=0.100.0
2
2
  sqlalchemy>=2.0.0
3
+ greenlet>=3.0.0
3
4
  fastapi_pagination>=0.12.12
4
5
  pydantic>=2.0.0
@@ -113,3 +113,59 @@ def test_unsupported_text_fail_safe() -> None:
113
113
  assert (
114
114
  ModelManager._matches_partial_index_where(condition, {"kind": "extra"}) is True
115
115
  )
116
+
117
+
118
+ def test_text_clause_is_null_matches() -> None:
119
+ condition = text("group_id IS NULL")
120
+ assert (
121
+ ModelManager._matches_partial_index_where(condition, {"group_id": None})
122
+ is True
123
+ )
124
+ assert (
125
+ ModelManager._matches_partial_index_where(condition, {"group_id": 1}) is False
126
+ )
127
+ # column missing from the object being validated: fail safe to "matches"
128
+ assert ModelManager._matches_partial_index_where(condition, {}) is True
129
+
130
+
131
+ def test_text_clause_is_not_null_matches() -> None:
132
+ condition = text(" group_id is not null ")
133
+ assert (
134
+ ModelManager._matches_partial_index_where(condition, {"group_id": None})
135
+ is False
136
+ )
137
+ assert (
138
+ ModelManager._matches_partial_index_where(condition, {"group_id": 1}) is True
139
+ )
140
+
141
+
142
+ def test_binary_expression_is_null_matches() -> None:
143
+ index = Index(
144
+ "ix_is_null",
145
+ _Model.group_id,
146
+ unique=True,
147
+ postgresql_where=(_Model.kind.is_(None)),
148
+ )
149
+ condition = index.dialect_options["postgresql"]["where"]
150
+ assert (
151
+ ModelManager._matches_partial_index_where(condition, {"kind": None}) is True
152
+ )
153
+ assert (
154
+ ModelManager._matches_partial_index_where(condition, {"kind": "main"}) is False
155
+ )
156
+
157
+
158
+ def test_binary_expression_is_not_null_matches() -> None:
159
+ index = Index(
160
+ "ix_is_not_null",
161
+ _Model.group_id,
162
+ unique=True,
163
+ postgresql_where=(_Model.kind.isnot(None)),
164
+ )
165
+ condition = index.dialect_options["postgresql"]["where"]
166
+ assert (
167
+ ModelManager._matches_partial_index_where(condition, {"kind": None}) is False
168
+ )
169
+ assert (
170
+ ModelManager._matches_partial_index_where(condition, {"kind": "main"}) is True
171
+ )
@@ -1,34 +0,0 @@
1
- # This workflow will upload a Python Package using Twine when a release is created
2
- # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3
-
4
- name: Upload Python Package
5
-
6
- on:
7
- release:
8
- types: [published]
9
-
10
- permissions:
11
- contents: read
12
-
13
- jobs:
14
- deploy:
15
-
16
- runs-on: ubuntu-latest
17
-
18
- steps:
19
- - uses: actions/checkout@v3
20
- - name: Set up Python
21
- uses: actions/setup-python@v3
22
- with:
23
- python-version: '3.x'
24
- - name: Install dependencies
25
- run: |
26
- python -m pip install --upgrade pip
27
- pip install build
28
- - name: Build package
29
- run: python -m build
30
- - name: Publish package
31
- uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
32
- with:
33
- user: __token__
34
- password: ${{ secrets.PYPI_API_TOKEN }}