TypeDAL 3.9.3__py3-none-any.whl → 3.10.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.

Potentially problematic release.


This version of TypeDAL might be problematic. Click here for more details.

typedal/__about__.py CHANGED
@@ -5,4 +5,4 @@ This file contains the Version info for this package.
5
5
  # SPDX-FileCopyrightText: 2023-present Robin van der Noord <robinvandernoord@gmail.com>
6
6
  #
7
7
  # SPDX-License-Identifier: MIT
8
- __version__ = "3.9.3"
8
+ __version__ = "3.10.0"
typedal/for_py4web.py CHANGED
@@ -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
 
typedal/mixins.py CHANGED
@@ -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
- def generate_slug_before_insert(row: OpRow) -> None:
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
- if suffix_len := settings["slug_suffix"]:
157
- generated_slug += f"-{slug_random_suffix(suffix_len)}"
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
- row["slug"] = slugify(generated_slug)
225
+ current_requires.append(HAS_UNIQUE_SLUG(db, f"{cls}.slug"))
160
226
 
161
- cls._before_insert.append(generate_slug_before_insert)
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]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: TypeDAL
3
- Version: 3.9.3
3
+ Version: 3.10.0
4
4
  Summary: Typing support for PyDAL
5
5
  Project-URL: Documentation, https://typedal.readthedocs.io/
6
6
  Project-URL: Issues, https://github.com/trialandsuccess/TypeDAL/issues
@@ -1,19 +1,19 @@
1
- typedal/__about__.py,sha256=1UM5faaqWwuDvGwM1phck1IYGhiBNMkmGgIU-_A9xpI,206
1
+ typedal/__about__.py,sha256=AACG7JiVoeqMLcZgS2LtqBNwIJ7iclXp2FZddjUMT4o,207
2
2
  typedal/__init__.py,sha256=QQpLiVl9w9hm2LBxey49Y_tCF_VB2bScVaS_mCjYy54,366
3
3
  typedal/caching.py,sha256=SMcJsahLlZ79yykWCveERFx1ZJUNEKhA9SPmCTIuLp8,11798
4
4
  typedal/cli.py,sha256=wzyId6YwRyqfuJ2byxvl6YecDNaKpkLmo-R5HvRuTok,19265
5
5
  typedal/config.py,sha256=0qy1zrTUdtmXPM9jHzFnSR1DJsqGJqcdG6pvhzKQHe0,11625
6
6
  typedal/core.py,sha256=OBokUmNBvm83REIEKUjjIXna5UWSXC5H8M82LiAag0M,102729
7
7
  typedal/fields.py,sha256=A4qt0aK4F_-UeOY-xJ0ObVY-tFEoLFy7TYRMHnp4g6o,6516
8
- typedal/for_py4web.py,sha256=jFy-sB_0x8h7xYrYuXjB4FnBc4gTOywnYPkX_OrXtM8,1904
8
+ typedal/for_py4web.py,sha256=plb08K6HmxbM1_5GuBFFzovAJIT7Q3g9ocNjUJOBngY,1946
9
9
  typedal/for_web2py.py,sha256=xn7zo6ImsmTkH6LacbjLQl2oqyBvP0zLqRxEJvMQk1w,1929
10
10
  typedal/helpers.py,sha256=uej96exzJ9qVBd6LudP8uJ_7Cmq6vKraerdKvSRBVjc,8128
11
- typedal/mixins.py,sha256=6QfeVBAk7J4uHSR82Ek_Bhq43hzxnrmLn6xqxZqYtMk,5757
11
+ typedal/mixins.py,sha256=ySF55URmSMyUCdMhiQJSKjkz4s5XGaO1pvmpJAOLUSU,7821
12
12
  typedal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  typedal/types.py,sha256=1FIgv1s0be0E8r5Wd9E1nvDvK4ETV8u2NlfI7_P6UUY,6752
14
14
  typedal/web2py_py4web_shared.py,sha256=VK9T8P5UwVLvfNBsY4q79ANcABv-jX76YKADt1Zz_co,1539
15
15
  typedal/serializers/as_json.py,sha256=ffo152W-sARYXym4BzwX709rrO2-QwKk2KunWY8RNl4,2229
16
- typedal-3.9.3.dist-info/METADATA,sha256=B7ZYk4T7UQ2lxTFa4oELN5Z3qV_1soCBCz0HzUWW7bg,10439
17
- typedal-3.9.3.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
18
- typedal-3.9.3.dist-info/entry_points.txt,sha256=m1wqcc_10rHWPdlQ71zEkmJDADUAnZtn7Jac_6mbyUc,44
19
- typedal-3.9.3.dist-info/RECORD,,
16
+ typedal-3.10.0.dist-info/METADATA,sha256=c2y2fbOyjCNK7HQUIh73-rPPgxFNIi6GJbAaR2Y2Yfw,10440
17
+ typedal-3.10.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
18
+ typedal-3.10.0.dist-info/entry_points.txt,sha256=m1wqcc_10rHWPdlQ71zEkmJDADUAnZtn7Jac_6mbyUc,44
19
+ typedal-3.10.0.dist-info/RECORD,,