TypeDAL 3.8.1__tar.gz → 3.8.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.
Potentially problematic release.
This version of TypeDAL might be problematic. Click here for more details.
- {typedal-3.8.1 → typedal-3.8.2}/CHANGELOG.md +4 -0
- {typedal-3.8.1 → typedal-3.8.2}/PKG-INFO +1 -1
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/__about__.py +1 -1
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/core.py +15 -6
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/for_web2py.py +2 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/mixins.py +17 -6
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_config.py +13 -9
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_helpers.py +5 -4
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_mixins.py +35 -1
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_relationships.py +20 -5
- {typedal-3.8.1 → typedal-3.8.2}/.github/workflows/su6.yml +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/.gitignore +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/.readthedocs.yml +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/README.md +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/coverage.svg +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/docs/1_getting_started.md +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/docs/2_defining_tables.md +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/docs/3_building_queries.md +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/docs/4_relationships.md +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/docs/5_py4web.md +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/docs/6_migrations.md +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/docs/7_mixins.md +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/docs/css/code_blocks.css +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/docs/index.md +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/docs/requirements.txt +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/example_new.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/example_old.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/mkdocs.yml +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/pyproject.toml +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/__init__.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/caching.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/cli.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/config.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/fields.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/for_py4web.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/helpers.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/py.typed +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/serializers/as_json.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/types.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/__init__.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/configs/simple.toml +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/configs/valid.env +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/configs/valid.toml +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_cli.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_docs_examples.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_json.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_main.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_mypy.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_orm.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_py4web.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_query_builder.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_row.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_stats.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_table.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_web2py.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/test_xx_others.py +0 -0
- {typedal-3.8.1 → typedal-3.8.2}/tests/timings.py +0 -0
|
@@ -279,9 +279,10 @@ def relationship(_type: To_Type, condition: Condition = None, join: JOIN_OPTIONS
|
|
|
279
279
|
)
|
|
280
280
|
|
|
281
281
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
T_Field: typing.TypeAlias = typing.Union["TypedField[Any]", "Table", Type["TypedTable"]]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _generate_relationship_condition(_: Type["TypedTable"], key: str, field: T_Field) -> Condition:
|
|
285
286
|
origin = typing.get_origin(field)
|
|
286
287
|
# else: generic
|
|
287
288
|
|
|
@@ -299,7 +300,7 @@ def _generate_relationship_condition(
|
|
|
299
300
|
def to_relationship(
|
|
300
301
|
cls: Type["TypedTable"] | type[Any],
|
|
301
302
|
key: str,
|
|
302
|
-
field:
|
|
303
|
+
field: T_Field,
|
|
303
304
|
) -> typing.Optional[Relationship[Any]]:
|
|
304
305
|
"""
|
|
305
306
|
Used to automatically create relationship instance for reference fields.
|
|
@@ -317,9 +318,14 @@ def to_relationship(
|
|
|
317
318
|
Also works for list:reference (list[OtherTable]) and TypedField[OtherTable].
|
|
318
319
|
"""
|
|
319
320
|
if looks_like(field, TypedField):
|
|
321
|
+
# typing.get_args works for list[str] but not for TypedField[role] :(
|
|
320
322
|
if args := typing.get_args(field):
|
|
323
|
+
# TypedField[SomeType] -> SomeType
|
|
321
324
|
field = args[0]
|
|
322
|
-
|
|
325
|
+
elif hasattr(field, "_type"):
|
|
326
|
+
# TypedField(SomeType) -> SomeType
|
|
327
|
+
field = typing.cast(T_Field, field._type)
|
|
328
|
+
else: # pragma: no cover
|
|
323
329
|
# weird
|
|
324
330
|
return None
|
|
325
331
|
|
|
@@ -543,13 +549,16 @@ class TypeDAL(pydal.DAL): # type: ignore
|
|
|
543
549
|
]
|
|
544
550
|
|
|
545
551
|
# add implicit relationships:
|
|
546
|
-
# User; list[User]; TypedField[User]; TypedField[list[User]]
|
|
552
|
+
# User; list[User]; TypedField[User]; TypedField[list[User]]; TypedField(User); TypedField(list[User])
|
|
547
553
|
relationships |= {
|
|
548
554
|
k: new_relationship
|
|
549
555
|
for k in reference_field_keys
|
|
550
556
|
if k not in relationships and (new_relationship := to_relationship(cls, k, annotations[k]))
|
|
551
557
|
}
|
|
552
558
|
|
|
559
|
+
# fixme: list[Reference] is recognized as relationship,
|
|
560
|
+
# TypedField(list[Reference]) is NOT recognized!!!
|
|
561
|
+
|
|
553
562
|
cache_dependency = self._config.caching and kwargs.pop("cache_dependency", True)
|
|
554
563
|
|
|
555
564
|
table: Table = self.define_table(tablename, *fields.values(), **kwargs)
|
|
@@ -36,6 +36,14 @@ class Mixin(_TypedTable):
|
|
|
36
36
|
('inconsistent method resolution' or 'metaclass conflicts')
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
|
+
__settings__: typing.ClassVar[dict[str, Any]]
|
|
40
|
+
|
|
41
|
+
def __init_subclass__(cls, **kwargs: Any):
|
|
42
|
+
"""
|
|
43
|
+
Ensures __settings__ exists for other mixins.
|
|
44
|
+
"""
|
|
45
|
+
cls.__settings__ = getattr(cls, "__settings__", None) or {}
|
|
46
|
+
|
|
39
47
|
|
|
40
48
|
class TimestampsMixin(Mixin):
|
|
41
49
|
"""
|
|
@@ -102,12 +110,16 @@ class SlugMixin(Mixin):
|
|
|
102
110
|
},
|
|
103
111
|
) # set via init subclass
|
|
104
112
|
|
|
105
|
-
def __init_subclass__(
|
|
113
|
+
def __init_subclass__(
|
|
114
|
+
cls, slug_field: str = None, slug_suffix_length: int = 0, slug_suffix: Optional[int] = None, **kw: Any
|
|
115
|
+
) -> None:
|
|
106
116
|
"""
|
|
107
117
|
Bind 'slug field' option to be used later (on_define).
|
|
108
118
|
|
|
109
119
|
You can control the length of the random suffix with the `slug_suffix_length` option (0 is no suffix).
|
|
110
120
|
"""
|
|
121
|
+
super().__init_subclass__(**kw)
|
|
122
|
+
|
|
111
123
|
# unfortunately, PyCharm and mypy do not recognize/autocomplete/typecheck init subclass (keyword) arguments.
|
|
112
124
|
if slug_field is None:
|
|
113
125
|
raise ValueError(
|
|
@@ -115,7 +127,7 @@ class SlugMixin(Mixin):
|
|
|
115
127
|
"e.g. `class MyClass(TypedTable, SlugMixin, slug_field='title'): ...`"
|
|
116
128
|
)
|
|
117
129
|
|
|
118
|
-
if
|
|
130
|
+
if slug_suffix:
|
|
119
131
|
warnings.warn(
|
|
120
132
|
"The 'slug_suffix' option is deprecated, use 'slug_suffix_length' instead.",
|
|
121
133
|
DeprecationWarning,
|
|
@@ -123,10 +135,9 @@ class SlugMixin(Mixin):
|
|
|
123
135
|
|
|
124
136
|
slug_suffix = slug_suffix_length or kw.get("slug_suffix", 0)
|
|
125
137
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
138
|
+
# append settings:
|
|
139
|
+
cls.__settings__["slug_field"] = slug_field
|
|
140
|
+
cls.__settings__["slug_suffix"] = slug_suffix
|
|
130
141
|
|
|
131
142
|
@classmethod
|
|
132
143
|
def __on_define__(cls, db: TypeDAL) -> None:
|
|
@@ -1,24 +1,25 @@
|
|
|
1
|
+
import datetime as dt
|
|
1
2
|
import os
|
|
2
3
|
import shutil
|
|
3
4
|
import tempfile
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
import datetime as dt
|
|
6
5
|
import uuid
|
|
6
|
+
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from pydal2sql import generate_sql
|
|
9
8
|
import pytest
|
|
9
|
+
|
|
10
10
|
# from contextlib import chdir
|
|
11
11
|
from contextlib_chdir import chdir
|
|
12
|
+
from pydal2sql import generate_sql
|
|
12
13
|
from testcontainers.postgres import PostgresContainer
|
|
13
14
|
|
|
14
|
-
from src.typedal import TypeDAL,
|
|
15
|
+
from src.typedal import TypeDAL, TypedField, TypedTable
|
|
15
16
|
from src.typedal.config import (
|
|
16
17
|
_load_dotenv,
|
|
17
18
|
_load_toml,
|
|
18
19
|
expand_env_vars_into_toml_values,
|
|
19
20
|
load_config,
|
|
20
21
|
)
|
|
21
|
-
from src.typedal.fields import
|
|
22
|
+
from src.typedal.fields import PointField, TimestampField, UUIDField
|
|
22
23
|
|
|
23
24
|
postgres = PostgresContainer(
|
|
24
25
|
dbname="postgres",
|
|
@@ -58,10 +59,12 @@ def test_load_toml(at_temp_dir):
|
|
|
58
59
|
base = Path("pyproject.toml")
|
|
59
60
|
base.write_text("# empty")
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
with pytest.warns(UserWarning):
|
|
63
|
+
# it should warn because the toml is empty
|
|
64
|
+
assert _load_toml(False) == ("", {})
|
|
65
|
+
assert _load_toml(None) == (str(base.resolve().absolute()), {})
|
|
66
|
+
assert _load_toml(str(base)) == ("pyproject.toml", {})
|
|
67
|
+
assert _load_toml(".") == (str(base.resolve().absolute()), {})
|
|
65
68
|
|
|
66
69
|
|
|
67
70
|
def test_load_dotenv(at_temp_dir):
|
|
@@ -160,6 +163,7 @@ def test_expand_env_vars():
|
|
|
160
163
|
|
|
161
164
|
# note: these are not really 'config' specific but we already have access to postgres here so good enough:
|
|
162
165
|
|
|
166
|
+
|
|
163
167
|
def test_timestamp_fields_sqlite(at_temp_dir):
|
|
164
168
|
db = TypeDAL("sqlite:memory")
|
|
165
169
|
|
|
@@ -11,6 +11,9 @@ from src.typedal.helpers import (
|
|
|
11
11
|
all_annotations,
|
|
12
12
|
as_lambda,
|
|
13
13
|
extract_type_optional,
|
|
14
|
+
get_db,
|
|
15
|
+
get_field,
|
|
16
|
+
get_table,
|
|
14
17
|
instanciate,
|
|
15
18
|
is_union,
|
|
16
19
|
looks_like,
|
|
@@ -18,7 +21,7 @@ from src.typedal.helpers import (
|
|
|
18
21
|
mktable,
|
|
19
22
|
origin_is_subclass,
|
|
20
23
|
to_snake,
|
|
21
|
-
unwrap_type,
|
|
24
|
+
unwrap_type,
|
|
22
25
|
)
|
|
23
26
|
from typedal import TypeDAL, TypedTable
|
|
24
27
|
from typedal.types import Field
|
|
@@ -202,7 +205,5 @@ def test_get_functions():
|
|
|
202
205
|
assert isinstance(table, pydal.objects.Table)
|
|
203
206
|
assert not isinstance(table, TypedTable)
|
|
204
207
|
field = get_field(TestGetFunctions.string)
|
|
205
|
-
print(
|
|
206
|
-
type(field)
|
|
207
|
-
)
|
|
208
|
+
print(type(field))
|
|
208
209
|
assert isinstance(field, Field)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import time
|
|
2
|
+
import uuid
|
|
2
3
|
from datetime import datetime
|
|
3
4
|
from typing import Optional
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
7
8
|
from src.typedal import TypeDAL, TypedTable
|
|
8
|
-
from src.typedal.
|
|
9
|
+
from src.typedal.fields import StringField, TypedField, UUIDField
|
|
10
|
+
from src.typedal.mixins import Mixin, SlugMixin, TimestampsMixin
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class AllMixins(TypedTable, SlugMixin, TimestampsMixin, slug_field="name"):
|
|
@@ -93,3 +95,35 @@ def test_reusing(db):
|
|
|
93
95
|
|
|
94
96
|
assert str(TableWithTimestamps.created_at) == "table_with_timestamps.created_at"
|
|
95
97
|
assert str(TableWithTimestamps.unrelated) == "table_with_timestamps.unrelated"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_combining_mixins():
|
|
101
|
+
class FirstMixin(Mixin):
|
|
102
|
+
def __init_subclass__(cls, first: str, **kw):
|
|
103
|
+
super().__init_subclass__(**kw)
|
|
104
|
+
|
|
105
|
+
cls.__settings__["first"] = first
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def one(cls):
|
|
109
|
+
return cls.__settings__["first"] == "first" and cls.__settings__["second"] == "second"
|
|
110
|
+
|
|
111
|
+
class SecondMixin(Mixin):
|
|
112
|
+
def __init_subclass__(cls, second: str, **kw):
|
|
113
|
+
super().__init_subclass__(**kw)
|
|
114
|
+
|
|
115
|
+
cls.__settings__["second"] = second
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def two(cls):
|
|
119
|
+
return cls.__settings__["first"] == "first" and cls.__settings__["second"] == "second"
|
|
120
|
+
|
|
121
|
+
class Combined(TypedTable, FirstMixin, SecondMixin, first="first", second="second"): ...
|
|
122
|
+
|
|
123
|
+
assert Combined.one()
|
|
124
|
+
assert Combined.two()
|
|
125
|
+
|
|
126
|
+
class CombinedDifferentOrder(TypedTable, SecondMixin, FirstMixin, first="first", second="second"): ...
|
|
127
|
+
|
|
128
|
+
assert CombinedDifferentOrder.one()
|
|
129
|
+
assert CombinedDifferentOrder.two()
|
|
@@ -45,6 +45,8 @@ class User(TypedTable, TaggableMixin):
|
|
|
45
45
|
gid = TypedField(str, default=uuid4)
|
|
46
46
|
name: str
|
|
47
47
|
roles: TypedField[list[Role]]
|
|
48
|
+
main_role = TypedField(Role)
|
|
49
|
+
extra_roles = TypedField(list[Role])
|
|
48
50
|
|
|
49
51
|
# relationships:
|
|
50
52
|
articles = relationship(list["Article"], lambda self, other: other.author == self.id)
|
|
@@ -102,9 +104,9 @@ def _setup_data():
|
|
|
102
104
|
|
|
103
105
|
reader, writer, editor = User.bulk_insert(
|
|
104
106
|
[
|
|
105
|
-
{"name": "Reader 1", "roles": [reader]},
|
|
106
|
-
{"name": "Writer 1", "roles": [reader, writer]},
|
|
107
|
-
{"name": "Editor 1", "roles": [reader, writer, editor]},
|
|
107
|
+
{"name": "Reader 1", "roles": [reader], "main_role": reader, "extra_roles": []},
|
|
108
|
+
{"name": "Writer 1", "roles": [reader, writer], "main_role": writer, "extra_roles": []},
|
|
109
|
+
{"name": "Editor 1", "roles": [reader, writer, editor], "main_role": editor, "extra_roles": []},
|
|
108
110
|
]
|
|
109
111
|
)
|
|
110
112
|
|
|
@@ -333,9 +335,22 @@ class CacheTwoRelationships(TypedTable):
|
|
|
333
335
|
second: NoCacheSecond
|
|
334
336
|
|
|
335
337
|
|
|
336
|
-
def
|
|
337
|
-
|
|
338
|
+
def test_relationship_detection():
|
|
339
|
+
user_table_relationships = User.get_relationships()
|
|
340
|
+
|
|
341
|
+
assert user_table_relationships["roles"]
|
|
342
|
+
assert user_table_relationships["main_role"]
|
|
343
|
+
assert user_table_relationships["extra_roles"]
|
|
344
|
+
assert user_table_relationships["articles"]
|
|
345
|
+
assert user_table_relationships["bestie"]
|
|
346
|
+
assert user_table_relationships["tags"]
|
|
338
347
|
|
|
348
|
+
assert user_table_relationships["roles"].join == "left"
|
|
349
|
+
assert user_table_relationships["main_role"].join == "inner"
|
|
350
|
+
assert user_table_relationships["extra_roles"].join == "left"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_caching():
|
|
339
354
|
uncached = User.join().collect_or_fail()
|
|
340
355
|
cached = User.cache().join().collect_or_fail() # not actually cached yet!
|
|
341
356
|
cached_user_only = User.join().cache(User.id).collect_or_fail() # idem
|
|
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
|