TypeDAL 3.9.3__tar.gz → 3.10.0__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.
Potentially problematic release.
This version of TypeDAL might be problematic. Click here for more details.
- {typedal-3.9.3 → typedal-3.10.0}/CHANGELOG.md +12 -0
- {typedal-3.9.3 → typedal-3.10.0}/PKG-INFO +1 -1
- {typedal-3.9.3 → typedal-3.10.0}/pyproject.toml +1 -1
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/__about__.py +1 -1
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/for_py4web.py +2 -2
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/mixins.py +75 -9
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_mixins.py +36 -3
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_py4web.py +20 -2
- {typedal-3.9.3 → typedal-3.10.0}/.github/workflows/su6.yml +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/.gitignore +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/.readthedocs.yml +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/README.md +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/coverage.svg +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/docs/1_getting_started.md +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/docs/2_defining_tables.md +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/docs/3_building_queries.md +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/docs/4_relationships.md +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/docs/5_py4web.md +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/docs/6_migrations.md +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/docs/7_mixins.md +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/docs/css/code_blocks.css +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/docs/index.md +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/docs/requirements.txt +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/example_new.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/example_old.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/mkdocs.yml +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/__init__.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/caching.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/cli.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/config.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/core.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/fields.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/for_web2py.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/helpers.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/py.typed +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/serializers/as_json.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/types.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/__init__.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/configs/simple.toml +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/configs/valid.env +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/configs/valid.toml +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_cli.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_config.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_docs_examples.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_helpers.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_json.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_main.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_mypy.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_orm.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_query_builder.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_relationships.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_row.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_stats.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_table.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_web2py.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/test_xx_others.py +0 -0
- {typedal-3.9.3 → typedal-3.10.0}/tests/timings.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v3.10.0 (2025-02-17)
|
|
6
|
+
|
|
7
|
+
### Feature
|
|
8
|
+
|
|
9
|
+
* Add a pydal validator to tables using the slug mixin (without a random suffix) to catch duplicates before the actual database insert (which raises a unique violation exception) ([`6466345`](https://github.com/trialandsuccess/TypeDAL/commit/64663454eab7a4f281660cb2df6e50e2dadd7740))
|
|
10
|
+
|
|
11
|
+
## v3.9.4 (2024-11-10)
|
|
12
|
+
|
|
13
|
+
### Fix
|
|
14
|
+
|
|
15
|
+
* **p4w:** Calling `for_py4web.DAL()` without any arguments (to load from config) should work even with singleton ([`16f9e68`](https://github.com/trialandsuccess/TypeDAL/commit/16f9e681eaac2fccd06a110031c0d55261a0a7e9))
|
|
16
|
+
|
|
5
17
|
## v3.9.3 (2024-11-08)
|
|
6
18
|
|
|
7
19
|
### Fix
|
|
@@ -8,7 +8,7 @@ dynamic = ["version"]
|
|
|
8
8
|
description = 'Typing support for PyDAL'
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
|
-
|
|
11
|
+
license-expression = "MIT"
|
|
12
12
|
keywords = []
|
|
13
13
|
authors = [
|
|
14
14
|
{ name = "Robin van der Noord", email = "contact@trialandsuccess.nl" },
|
|
@@ -24,8 +24,8 @@ class Fixture(_Fixture): # type: ignore
|
|
|
24
24
|
class PY4WEB_DAL_SINGLETON(MetaDAL):
|
|
25
25
|
_instances: typing.ClassVar[typing.MutableMapping[str, AnyType]] = {}
|
|
26
26
|
|
|
27
|
-
def __call__(cls, uri: str, *args: typing.Any, **kwargs: typing.Any) -> AnyType:
|
|
28
|
-
db_uid = kwargs.get("db_uid", hashlib_md5(repr(uri)).hexdigest())
|
|
27
|
+
def __call__(cls, uri: typing.Optional[str] = None, *args: typing.Any, **kwargs: typing.Any) -> AnyType:
|
|
28
|
+
db_uid = kwargs.get("db_uid", hashlib_md5(repr(uri or (args, kwargs))).hexdigest())
|
|
29
29
|
if db_uid not in cls._instances:
|
|
30
30
|
cls._instances[db_uid] = super().__call__(uri, *args, **kwargs)
|
|
31
31
|
|
|
@@ -11,6 +11,8 @@ import warnings
|
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
from typing import Any, Optional
|
|
13
13
|
|
|
14
|
+
from pydal import DAL
|
|
15
|
+
from pydal.validators import IS_NOT_IN_DB, ValidationError
|
|
14
16
|
from slugify import slugify
|
|
15
17
|
|
|
16
18
|
from .core import ( # noqa F401 - used by example in docstring
|
|
@@ -87,6 +89,56 @@ def slug_random_suffix(length: int = 8) -> str:
|
|
|
87
89
|
return base64.urlsafe_b64encode(os.urandom(length)).rstrip(b"=").decode().strip("=")
|
|
88
90
|
|
|
89
91
|
|
|
92
|
+
T = typing.TypeVar("T")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# noinspection PyPep8Naming
|
|
96
|
+
class HAS_UNIQUE_SLUG(IS_NOT_IN_DB):
|
|
97
|
+
"""
|
|
98
|
+
Checks if slugified field is already in the db.
|
|
99
|
+
|
|
100
|
+
Usage:
|
|
101
|
+
table.name = HAS_UNIQUE_SLUG(db, "table.slug")
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
db: TypeDAL | DAL,
|
|
107
|
+
field: str, # table.slug
|
|
108
|
+
error_message: str = "This slug is not unique: %s.",
|
|
109
|
+
):
|
|
110
|
+
"""
|
|
111
|
+
Based on IS_NOT_IN_DB but with less options and a different default error message.
|
|
112
|
+
"""
|
|
113
|
+
super().__init__(db, field, error_message)
|
|
114
|
+
|
|
115
|
+
def validate(self, original: T, record_id: Optional[int] = None) -> T:
|
|
116
|
+
"""
|
|
117
|
+
Performs checks to see if the slug already exists for a different row.
|
|
118
|
+
"""
|
|
119
|
+
value = slugify(str(original))
|
|
120
|
+
if not value.strip():
|
|
121
|
+
raise ValidationError(self.translator(self.error_message))
|
|
122
|
+
|
|
123
|
+
(tablename, fieldname) = str(self.field).split(".")
|
|
124
|
+
table = self.dbset.db[tablename]
|
|
125
|
+
field = table[fieldname]
|
|
126
|
+
query = field == value
|
|
127
|
+
|
|
128
|
+
# make sure exclude the record_id
|
|
129
|
+
row_id = record_id or self.record_id
|
|
130
|
+
if isinstance(row_id, dict): # pragma: no cover
|
|
131
|
+
row_id = table(**row_id)
|
|
132
|
+
if row_id is not None:
|
|
133
|
+
query &= table._id != row_id
|
|
134
|
+
subset = self.dbset(query)
|
|
135
|
+
|
|
136
|
+
if subset.count():
|
|
137
|
+
raise ValidationError(self.error_message % value)
|
|
138
|
+
|
|
139
|
+
return original
|
|
140
|
+
|
|
141
|
+
|
|
90
142
|
class SlugMixin(Mixin):
|
|
91
143
|
"""
|
|
92
144
|
(Opinionated) example mixin to add a 'slug' field, which depends on a user-provided other field.
|
|
@@ -139,26 +191,40 @@ class SlugMixin(Mixin):
|
|
|
139
191
|
cls.__settings__["slug_field"] = slug_field
|
|
140
192
|
cls.__settings__["slug_suffix"] = slug_suffix
|
|
141
193
|
|
|
194
|
+
@classmethod
|
|
195
|
+
def __generate_slug_before_insert(cls, row: OpRow) -> None:
|
|
196
|
+
settings = cls.__settings__
|
|
197
|
+
|
|
198
|
+
text_input = row[settings["slug_field"]]
|
|
199
|
+
generated_slug = slugify(text_input)
|
|
200
|
+
|
|
201
|
+
if suffix_len := settings["slug_suffix"]:
|
|
202
|
+
generated_slug += f"-{slug_random_suffix(suffix_len)}"
|
|
203
|
+
|
|
204
|
+
row["slug"] = slugify(generated_slug)
|
|
205
|
+
return None
|
|
206
|
+
|
|
142
207
|
@classmethod
|
|
143
208
|
def __on_define__(cls, db: TypeDAL) -> None:
|
|
144
209
|
"""
|
|
145
210
|
When db is available, include a before_insert hook to generate and include a slug.
|
|
146
211
|
"""
|
|
147
212
|
super().__on_define__(db)
|
|
213
|
+
settings = cls.__settings__
|
|
148
214
|
|
|
149
215
|
# slugs should not be editable (for SEO reasons), so there is only a before insert hook:
|
|
150
|
-
|
|
151
|
-
settings = cls.__settings__
|
|
152
|
-
|
|
153
|
-
text_input = row[settings["slug_field"]]
|
|
154
|
-
generated_slug = slugify(text_input)
|
|
216
|
+
cls._before_insert.append(cls.__generate_slug_before_insert)
|
|
155
217
|
|
|
156
|
-
|
|
157
|
-
|
|
218
|
+
if settings["slug_suffix"] == 0:
|
|
219
|
+
# add a validator to the field that will be slugified:
|
|
220
|
+
slug_field = getattr(cls, settings["slug_field"])
|
|
221
|
+
current_requires = getattr(slug_field, "requires", None) or []
|
|
222
|
+
if not isinstance(current_requires, list):
|
|
223
|
+
current_requires = [current_requires]
|
|
158
224
|
|
|
159
|
-
|
|
225
|
+
current_requires.append(HAS_UNIQUE_SLUG(db, f"{cls}.slug"))
|
|
160
226
|
|
|
161
|
-
|
|
227
|
+
slug_field.requires = current_requires
|
|
162
228
|
|
|
163
229
|
@classmethod
|
|
164
230
|
def from_slug(cls: typing.Type[T_MetaInstance], slug: str, join: bool = True) -> Optional[T_MetaInstance]:
|
|
@@ -14,7 +14,12 @@ class AllMixins(TypedTable, SlugMixin, TimestampsMixin, slug_field="name"):
|
|
|
14
14
|
name: str
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class TableWithMixins(TypedTable, SlugMixin, slug_field="name", slug_suffix_length=
|
|
17
|
+
class TableWithMixins(TypedTable, SlugMixin, slug_field="name", slug_suffix_length=0):
|
|
18
|
+
name: str
|
|
19
|
+
number: Optional[int]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TableWithSlugSuffix(TypedTable, SlugMixin, slug_field="name", slug_suffix_length=1):
|
|
18
23
|
name: str
|
|
19
24
|
number: Optional[int]
|
|
20
25
|
|
|
@@ -43,6 +48,7 @@ def db():
|
|
|
43
48
|
|
|
44
49
|
_db.define(AllMixins)
|
|
45
50
|
_db.define(TableWithMixins)
|
|
51
|
+
_db.define(TableWithSlugSuffix)
|
|
46
52
|
_db.define(TableWithTimestamps)
|
|
47
53
|
yield _db
|
|
48
54
|
|
|
@@ -57,10 +63,17 @@ def test_order(db):
|
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
def test_slug(db):
|
|
60
|
-
row = TableWithMixins.
|
|
66
|
+
row, error = TableWithMixins.validate_and_insert(name="")
|
|
67
|
+
assert row is None
|
|
68
|
+
assert error
|
|
69
|
+
|
|
70
|
+
# without random suffix: duplicates are forbidden
|
|
71
|
+
|
|
72
|
+
row, error = TableWithMixins.validate_and_insert(name="Two Words")
|
|
73
|
+
assert error is None
|
|
61
74
|
|
|
62
75
|
assert row.name == "Two Words"
|
|
63
|
-
assert row.slug
|
|
76
|
+
assert row.slug == "two-words"
|
|
64
77
|
|
|
65
78
|
assert TableWithMixins.from_slug(row.slug)
|
|
66
79
|
assert TableWithMixins.from_slug("missing") is None
|
|
@@ -70,6 +83,26 @@ def test_slug(db):
|
|
|
70
83
|
with pytest.raises(ValueError):
|
|
71
84
|
TableWithMixins.from_slug_or_fail("missing")
|
|
72
85
|
|
|
86
|
+
row, error = TableWithMixins.validate_and_insert(name="Two Words")
|
|
87
|
+
assert row is None
|
|
88
|
+
assert error == {
|
|
89
|
+
'name': 'This slug is not unique: two-words.',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# with random suffix: duplicates are fine
|
|
93
|
+
|
|
94
|
+
row, error = TableWithSlugSuffix.validate_and_insert(name="Two Words")
|
|
95
|
+
assert error is None
|
|
96
|
+
|
|
97
|
+
assert row.name == "Two Words"
|
|
98
|
+
assert row.slug.startswith("two-words")
|
|
99
|
+
|
|
100
|
+
row, error = TableWithSlugSuffix.validate_and_insert(name="Two Words")
|
|
101
|
+
assert error is None
|
|
102
|
+
|
|
103
|
+
assert row.name == "Two Words"
|
|
104
|
+
assert row.slug.startswith("two-words")
|
|
105
|
+
|
|
73
106
|
|
|
74
107
|
def test_timestamps(db):
|
|
75
108
|
row = TableWithTimestamps.insert(unrelated="Hi")
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import tempfile
|
|
3
|
-
from
|
|
3
|
+
from contextlib_chdir import chdir
|
|
4
4
|
|
|
5
5
|
from pydal.validators import IS_EMAIL, IS_NOT_IN_DB
|
|
6
6
|
|
|
7
7
|
from src.typedal import TypedTable
|
|
8
8
|
from src.typedal.for_py4web import DAL, AuthUser, setup_py4web_tables
|
|
9
9
|
from src.typedal.serializers import as_json
|
|
10
|
+
from typedal.config import TypeDALConfig
|
|
10
11
|
|
|
11
12
|
db = DAL("sqlite:memory")
|
|
12
13
|
|
|
@@ -59,6 +60,15 @@ def test_py4web_dal_singleton():
|
|
|
59
60
|
db_2a = DAL("sqlite://test_py4web_dal_singleton", folder=d, enable_typedal_caching=False)
|
|
60
61
|
db_2b = DAL("sqlite://test_py4web_dal_singleton", folder=d, enable_typedal_caching=False)
|
|
61
62
|
|
|
63
|
+
conf = {
|
|
64
|
+
"database": "sqlite:memory",
|
|
65
|
+
"dialect": "sqlite",
|
|
66
|
+
"pyproject": "",
|
|
67
|
+
"flag_location": f"{d}/flags"
|
|
68
|
+
}
|
|
69
|
+
db_3a = DAL(config=TypeDALConfig.load(conf))
|
|
70
|
+
db_3b = DAL(config=TypeDALConfig.load(conf))
|
|
71
|
+
|
|
62
72
|
assert db_1a is db_1b
|
|
63
73
|
assert db_1a._uri == db_1b._uri
|
|
64
74
|
assert db_1a._db_uid == db_1b._db_uid
|
|
@@ -74,6 +84,14 @@ def test_py4web_dal_singleton():
|
|
|
74
84
|
assert db_1b is not db_2b
|
|
75
85
|
assert db_1b._uri != db_2b._uri
|
|
76
86
|
assert db_1b._db_uid != db_2b._db_uid
|
|
77
|
-
|
|
87
|
+
|
|
88
|
+
assert db_3a is db_3b
|
|
89
|
+
assert db_3a is not db_1a
|
|
90
|
+
assert db_3a is not db_2a
|
|
91
|
+
|
|
92
|
+
assert db_1a._uri == db_1b._uri
|
|
93
|
+
assert db_3a._uri == db_1a._uri
|
|
94
|
+
assert db_3a._uri != db_2a._uri
|
|
95
|
+
|
|
78
96
|
# reset singletons for later use:
|
|
79
97
|
DAL._clear()
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|