Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
75bb725
fix: prepare for python 3.14 which introduces annotationlib to parse …
robinvandernoord Jan 9, 2025
8ff6d73
docs: remove old annotation parsing comments that are irrelevant with…
robinvandernoord Jan 9, 2025
00ba039
chore: make mypy happy
robinvandernoord Jan 9, 2025
19cebd2
fix: upgrade to at least configuraptor 1.27.1 for python 3.14
robinvandernoord Jan 9, 2025
c8b6a03
refactor: black prettify
robinvandernoord Mar 25, 2025
b7df789
fix: better error for missing ForwardRef
robinvandernoord Mar 25, 2025
07040ad
fix: use custom patched pydal for python 3.14
robinvandernoord Mar 25, 2025
7910d8c
Merge branch 'master' of github.com:trialandsuccess/TypeDAL into pyth…
robinvandernoord Mar 25, 2025
3b3f49e
chore: add updated coverage.xml after running `pytest --html`
robinvandernoord Mar 25, 2025
f3bfacd
Merge branch 'master' of github.com:trialandsuccess/TypeDAL into pyth…
robinvandernoord Mar 25, 2025
74edf4c
fix: support explicit `ForwardRef()`
robinvandernoord Mar 25, 2025
dcf5aed
chore: make `su6` happier
robinvandernoord Mar 25, 2025
251d1ea
Merge branch 'master' of github.com:trialandsuccess/TypeDAL into pyth…
robinvandernoord Apr 25, 2025
56aaf05
Merge branch 'master' of github.com:trialandsuccess/TypeDAL into pyth…
robinvandernoord May 19, 2025
b518585
chore(gh): set 3.14 (pre) as highest python
robinvandernoord May 19, 2025
f14962c
fix: don't force slug if already manually set
robinvandernoord May 27, 2025
e15df9a
Merge branch 'master' of github.com:trialandsuccess/TypeDAL into pyth…
robinvandernoord Sep 19, 2025
1c23954
fix: fix 3.14-related issues in tests and bump typer to specific vers…
robinvandernoord Sep 19, 2025
d9191a4
Merge branch 'master' into python3.14
robinvandernoord Sep 19, 2025
b902005
feat: extend Expression functionality with Python 3.14 template strin…
robinvandernoord Sep 19, 2025
7d21200
feat: t-string support for `sql_expression` and `executesql` in Pytho…
robinvandernoord Sep 19, 2025
356e707
break: drop Python 3.10 support to reduce multi-version headaches
robinvandernoord Sep 19, 2025
6d668e9
Merge branch 'master' into python3.14
robinvandernoord Sep 20, 2025
e6d33e6
fix: support .orderby as alias for .select(orderby=
robinvandernoord Sep 24, 2025
e26da4e
refactor: split `TableDefinitionBuilder` functionality from core type…
robinvandernoord Sep 30, 2025
8bc6c7c
refactor: moved more stuff around :)
robinvandernoord Sep 30, 2025
6ac30d0
feat: support nested joins like .join('relationship.with_relationship')
robinvandernoord Sep 30, 2025
2a57301
feat: continued work for nested joins like .join('relationship.with_r…
robinvandernoord Sep 30, 2025
b2b2756
chore: refactor, make `su6` happy
robinvandernoord Sep 30, 2025
b933bf2
Merge branch 'refactor-core' of github.com:trialandsuccess/TypeDAL in…
robinvandernoord Oct 1, 2025
ae4ad9e
chore: mypy/ruff don't support 314 yet
robinvandernoord Oct 1, 2025
85c6dbf
chore: improved tests, typing and docs for latest features
robinvandernoord Oct 1, 2025
b4c836c
fix: bump pydal to version that supports 3.14
robinvandernoord Oct 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/su6.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: '3.11'
- uses: yezz123/setup-uv@v4
with:
uv-venv: ".venv"
Expand All @@ -25,7 +25,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.13'
python-version: '3.14'
allow-prereleases: true
- uses: yezz123/setup-uv@v4
with:
uv-venv: ".venv"
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2
build:
os: ubuntu-22.04
tools:
python: "3.10"
python: "3.11"

mkdocs:
configuration: mkdocs.yml
Expand Down
23 changes: 14 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.metadata]
allow-direct-references = true

[project]
name = "TypeDAL"
dynamic = ["version"]
description = 'Typing support for PyDAL'
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.11"
license-expression = "MIT"
keywords = []
authors = [
Expand All @@ -16,20 +19,21 @@ authors = [
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = [
"pydal <= 20250228.1", # core
"pydal >= 20251012.3", # core
"dill < 1", # caching
"configuraptor >= 1.26.2, < 2", # config
"configuraptor >= 1.27.1, < 2", # config
"Configurable-JSON < 2", # json dumping
"python-slugify < 9",
"legacy-cgi; python_version >= '3.13'"
"legacy-cgi; python_version >= '3.13'",
"python-dateutil < 3",
]

[project.optional-dependencies]
Expand All @@ -38,7 +42,7 @@ py4web = [
]

migrations = [
"typer",
"typer >=0.18, <0.19",
"tabulate",
"pydal2sql>=1.2.0",
"edwh-migrate>=0.8.0",
Expand All @@ -48,7 +52,7 @@ migrations = [

all = [
"py4web",
"typer",
"typer >=0.18, <0.19",
"tabulate",
"pydal2sql[all]>=1.2.0",
"edwh-migrate[full]>=0.8.0",
Expand Down Expand Up @@ -114,7 +118,7 @@ badge = true
mypy = "--disable-error-code misc"

[tool.black]
target-version = ["py310"]
target-version = ["py313"]
line-length = 120
# 'extend-exclude' excludes files or directories in addition to the defaults
extend-exclude = '''
Expand All @@ -130,6 +134,7 @@ extend-exclude = '''
[tool.coverage.report]
exclude_also = [
"if TYPE_CHECKING:",
"if t.TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
"except ImportError as e:",
"except ImportError:",
Expand All @@ -156,7 +161,7 @@ strict = true
exclude = ["venv", ".bak"]

[tool.ruff]
target-version = "py310"
target-version = "py313"
line-length = 120

extend-exclude = ["*.bak/", "venv*/"]
Expand Down
18 changes: 9 additions & 9 deletions src/typedal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@
TypeDAL Library.
"""

from . import fields
from .core import (
Relationship,
TypeDAL,
TypedField,
TypedRows,
TypedTable,
relationship,
)
from .core import TypeDAL
from .fields import TypedField
from .helpers import sql_expression
from .query_builder import QueryBuilder
from .relationships import Relationship, relationship
from .rows import TypedRows
from .tables import TypedTable

from . import fields # isort: skip

try:
from .for_py4web import DAL as P4W_DAL
except ImportError: # pragma: no cover
P4W_DAL = None # type: ignore

__all__ = [
"QueryBuilder",
"Relationship",
"TypeDAL",
"TypedField",
Expand Down
69 changes: 36 additions & 33 deletions src/typedal/caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,28 @@
"""

import contextlib
import datetime as dt
import hashlib
import json
import typing
from datetime import datetime, timedelta, timezone
from typing import Any, Iterable, Mapping, Optional, TypeVar
import typing as t

import dill # nosec
from pydal.objects import Field, Rows, Set

from .core import TypedField, TypedRows, TypedTable
from .fields import TypedField
from .rows import TypedRows
from .tables import TypedTable
from .types import Query

if typing.TYPE_CHECKING:
if t.TYPE_CHECKING:
from .core import TypeDAL


def get_now(tz: timezone = timezone.utc) -> datetime:
def get_now(tz: dt.timezone = dt.timezone.utc) -> dt.datetime:
"""
Get the default datetime, optionally in a specific timezone.
"""
return datetime.now(tz)
return dt.datetime.now(tz)


class _TypedalCache(TypedTable):
Expand All @@ -33,8 +34,8 @@ class _TypedalCache(TypedTable):

key: TypedField[str]
data: TypedField[bytes]
cached_at = TypedField(datetime, default=get_now)
expires_at: TypedField[datetime | None]
cached_at = TypedField(dt.datetime, default=get_now)
expires_at: TypedField[dt.datetime | None]


class _TypedalCacheDependency(TypedTable):
Expand All @@ -47,7 +48,7 @@ class _TypedalCacheDependency(TypedTable):
idx: TypedField[int]


def prepare(field: Any) -> str:
def prepare(field: t.Any) -> str:
"""
Prepare data to be used in a cache key.

Expand All @@ -56,18 +57,18 @@ def prepare(field: Any) -> str:
"""
if isinstance(field, str):
return field
elif isinstance(field, (dict, Mapping)):
elif isinstance(field, (dict, t.Mapping)):
data = {str(k): prepare(v) for k, v in field.items()}
return json.dumps(data, sort_keys=True)
elif isinstance(field, Iterable):
elif isinstance(field, t.Iterable):
return ",".join(sorted([prepare(_) for _ in field]))
elif isinstance(field, bool):
return str(int(field))
else:
return str(field)


def create_cache_key(*fields: Any) -> str:
def create_cache_key(*fields: t.Any) -> str:
"""
Turn any fields of data into a string.
"""
Expand All @@ -83,7 +84,7 @@ def hash_cache_key(cache_key: str | bytes) -> str:
return h.hexdigest()


def create_and_hash_cache_key(*fields: Any) -> tuple[str, str]:
def create_and_hash_cache_key(*fields: t.Any) -> tuple[str, str]:
"""
Combine the input fields into one key and hash it with SHA 256.
"""
Expand Down Expand Up @@ -112,7 +113,7 @@ def _get_dependency_ids(rows: Rows, dependency_keys: list[tuple[Field, str]]) ->
return dependencies


def _determine_dependencies_auto(_: TypedRows[Any], rows: Rows) -> DependencyTupleSet:
def _determine_dependencies_auto(_: TypedRows[t.Any], rows: Rows) -> DependencyTupleSet:
dependency_keys = []
for field in rows.fields:
if str(field).endswith(".id"):
Expand All @@ -123,7 +124,7 @@ def _determine_dependencies_auto(_: TypedRows[Any], rows: Rows) -> DependencyTup
return _get_dependency_ids(rows, dependency_keys)


def _determine_dependencies(instance: TypedRows[Any], rows: Rows, depends_on: list[Any]) -> DependencyTupleSet:
def _determine_dependencies(instance: TypedRows[t.Any], rows: Rows, depends_on: list[t.Any]) -> DependencyTupleSet:
if not depends_on:
return _determine_dependencies_auto(instance, rows)

Expand All @@ -144,11 +145,11 @@ def _determine_dependencies(instance: TypedRows[Any], rows: Rows, depends_on: li
return _get_dependency_ids(rows, dependency_keys)


def remove_cache(idx: int | Iterable[int], table: str) -> None:
def remove_cache(idx: int | t.Iterable[int], table: str) -> None:
"""
Remove any cache entries that are dependant on one or multiple indices of a table.
"""
if not isinstance(idx, Iterable):
if not isinstance(idx, t.Iterable):
idx = [idx]

related = (
Expand Down Expand Up @@ -184,23 +185,25 @@ def _remove_cache(s: Set, tablename: str) -> None:
remove_cache(indeces, tablename)


T_TypedTable = TypeVar("T_TypedTable", bound=TypedTable)
T_TypedTable = t.TypeVar("T_TypedTable", bound=TypedTable)


def get_expire(
expires_at: Optional[datetime] = None, ttl: Optional[int | timedelta] = None, now: Optional[datetime] = None
) -> datetime | None:
expires_at: t.Optional[dt.datetime] = None,
ttl: t.Optional[int | dt.timedelta] = None,
now: t.Optional[dt.datetime] = None,
) -> dt.datetime | None:
"""
Based on an expires_at date or a ttl (in seconds or a time delta), determine the expire date.
"""
now = now or get_now()

if expires_at and ttl:
raise ValueError("Please only supply an `expired at` date or a `ttl` in seconds!")
elif isinstance(ttl, timedelta):
elif isinstance(ttl, dt.timedelta):
return now + ttl
elif ttl:
return now + timedelta(seconds=ttl)
return now + dt.timedelta(seconds=ttl)
elif expires_at:
return expires_at

Expand All @@ -210,8 +213,8 @@ def get_expire(
def save_to_cache(
instance: TypedRows[T_TypedTable],
rows: Rows,
expires_at: Optional[datetime] = None,
ttl: Optional[int | timedelta] = None,
expires_at: t.Optional[dt.datetime] = None,
ttl: t.Optional[int | dt.timedelta] = None,
) -> TypedRows[T_TypedTable]:
"""
Save a typedrows result to the database, and save dependencies from rows.
Expand All @@ -237,13 +240,13 @@ def save_to_cache(
return instance


def _load_from_cache(key: str, db: "TypeDAL") -> Any | None:
def _load_from_cache(key: str, db: "TypeDAL") -> t.Any | None:
if not (row := _TypedalCache.where(key=key).first()):
return None

now = get_now()

expires = row.expires_at.replace(tzinfo=timezone.utc) if row.expires_at else None
expires = row.expires_at.replace(tzinfo=dt.timezone.utc) if row.expires_at else None

if expires and now >= expires:
row.delete_record()
Expand All @@ -261,7 +264,7 @@ def _load_from_cache(key: str, db: "TypeDAL") -> Any | None:
return inst


def load_from_cache(key: str, db: "TypeDAL") -> Any | None:
def load_from_cache(key: str, db: "TypeDAL") -> t.Any | None:
"""
If 'key' matches a non-expired row in the database, try to load the dill.

Expand Down Expand Up @@ -302,10 +305,10 @@ def _expired_and_valid_query() -> tuple[str, str]:
return expired_items, valid_items


T = typing.TypeVar("T")
Stats = typing.TypedDict("Stats", {"total": T, "valid": T, "expired": T})
T = t.TypeVar("T")
Stats = t.TypedDict("Stats", {"total": T, "valid": T, "expired": T})

RowStats = typing.TypedDict(
RowStats = t.TypedDict(
"RowStats",
{
"Dependent Cache Entries": int,
Expand Down Expand Up @@ -338,7 +341,7 @@ def row_stats(db: "TypeDAL", table: str, row_id: str) -> Stats[RowStats]:
}


TableStats = typing.TypedDict(
TableStats = t.TypedDict(
"TableStats",
{
"Dependent Cache Entries": int,
Expand Down Expand Up @@ -371,7 +374,7 @@ def table_stats(db: "TypeDAL", table: str) -> Stats[TableStats]:
}


GenericStats = typing.TypedDict(
GenericStats = t.TypedDict(
"GenericStats",
{
"entries": int,
Expand Down
Loading
Loading