Skip to content

Commit 0e37352

Browse files
authored
3.0.0a3 (#127)
- Define `use_scope` return type as a `dict` to prevent clashes between `django`, `django-stubs`, and `django-idom` - Bump django-idom version - Prevent `db_cleanup` from causing startup failure on any `DatabaseError`, rather than just `OperationalError(DatabaseError)` - `IDOM_DATABASE` setting - `IDOM_CACHE` setting (instead of implicitly relying on `CACHE["idom"] to exist - Rename `ComponentParams` to `ComponentSession` (we're likely to reuse it for other stuff in the future) - Bump IDOM to latest pre-release - Use new `VdomAttributes` and `key` syntax in `idom.html.*`.
1 parent 25b2c12 commit 0e37352

19 files changed

+202
-112
lines changed

CHANGELOG.md

+13-11
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Using the following categories, list your changes in this order:
3636

3737
- Nothing (yet)
3838

39-
## [3.0.0a2] - 2023-02-02
39+
## [3.0.0a3] - 2023-02-21
4040

4141
???+ note
4242

@@ -45,34 +45,36 @@ Using the following categories, list your changes in this order:
4545
To upgrade from previous version you will need to...
4646

4747
1. Install `django-idom >= 3.0.0`
48-
2. Run `idom update-html-usages <DIR>` to update your `idom.html.*` calls to the new syntax
48+
2. Run `idom rewrite-keys <DIR>` and `idom rewrite-camel-case-props <DIR>` to update your `idom.html.*` calls to the new syntax
4949
3. Run `python manage.py migrate` to create the new Django-IDOM database entries
5050

5151
### Added
5252

5353
- The `idom` client will automatically configure itself to debug mode depending on `settings.py:DEBUG`.
54-
- `use_connection` hook for returning the browser's active `Connection`
54+
- `use_connection` hook for returning the browser's active `Connection`.
55+
- `IDOM_CACHE` is now configurable within `settings.py` to whatever cache name you wish.
5556

5657
### Changed
5758

5859
- It is now mandatory to run `manage.py migrate` after installing IDOM.
59-
- Bumped the minimum IDOM version to 1.0.0
60-
- Due to IDOM 1.0.0, `idom.html.*`, HTML properties are now `snake_case` `**kwargs` rather than a `dict` of values.
61-
- You can auto-convert to the new style using `idom update-html-usages <DIR>`.
60+
- Bumped the minimum IDOM version to 1.0.0. Due to IDOM 1.0.0, `idom.html.*`...
61+
- HTML properties can now be `snake_case`. For example `className` now becomes `class_name`.
62+
- `key=...` is now declared within the props `dict` (rather than as a `kwarg`).
6263
- The `component` template tag now supports both positional and keyword arguments.
6364
- The `component` template tag now supports non-serializable arguments.
6465
- `IDOM_WS_MAX_RECONNECT_TIMEOUT` setting has been renamed to `IDOM_RECONNECT_MAX`.
6566

6667
### Removed
6768

6869
- `django_idom.hooks.use_websocket` has been removed. The similar replacement is `django_idom.hooks.use_connection`.
69-
- `django_idom.types.IdomWebsocket` has been removed. The similar replacement is `django_idom.types.Connection`
70+
- `django_idom.types.IdomWebsocket` has been removed. The similar replacement is `django_idom.types.Connection`.
71+
- `settings.py:CACHE['idom']` is no longer used by default. The name of the cache back-end must now be specified with the `IDOM_CACHE` setting.
7072

7173
### Fixed
7274

73-
- `view_to_component` will now retain any HTML that was defined in a `<head>` tag.
75+
- `view_to_component` will now retain the contents of a `<head>` tag when rendering.
7476
- React client is now set to `production` rather than `development`.
75-
- `use_query` will now utilize `field.related_name` when postprocessing many-to-one relationships
77+
- `use_query` will now utilize `field.related_name` when postprocessing many-to-one relationships.
7678

7779
### Security
7880

@@ -246,8 +248,8 @@ Using the following categories, list your changes in this order:
246248

247249
- Support for IDOM within the Django
248250

249-
[unreleased]: https://github.com/idom-team/django-idom/compare/3.0.0a2...HEAD
250-
[3.0.0a2]: https://github.com/idom-team/django-idom/compare/2.2.1...3.0.0a2
251+
[unreleased]: https://github.com/idom-team/django-idom/compare/3.0.0a3...HEAD
252+
[3.0.0a3]: https://github.com/idom-team/django-idom/compare/2.2.1...3.0.0a3
251253
[2.2.1]: https://github.com/idom-team/django-idom/compare/2.2.0...2.2.1
252254
[2.2.0]: https://github.com/idom-team/django-idom/compare/2.1.0...2.2.0
253255
[2.1.0]: https://github.com/idom-team/django-idom/compare/2.0.1...2.1.0

docs/python/settings.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# If "idom" cache is not configured, then "default" will be used
2-
# IDOM works best with a multiprocessing-safe and thread-safe cache backend.
3-
CACHES = {
4-
"idom": {"BACKEND": ...},
5-
}
1+
# IDOM requires a multiprocessing-safe and thread-safe cache.
2+
IDOM_CACHE = "default"
3+
4+
# IDOM requires a multiprocessing-safe and thread-safe database.
5+
IDOM_DATABASE = "default"
66

77
# Maximum seconds between reconnection attempts before giving up.
88
# Use `0` to prevent component reconnection.
@@ -11,5 +11,5 @@
1111
# The URL for IDOM to serve the component rendering websocket
1212
IDOM_WEBSOCKET_URL = "idom/"
1313

14-
# Dotted path to the default postprocessor function, or `None`
14+
# Dotted path to the default `django_idom.hooks.use_query` postprocessor function, or `None`
1515
IDOM_DEFAULT_QUERY_POSTPROCESSOR = "example_project.utils.my_postprocessor"

requirements/pkg-deps.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
channels >=4.0.0
2-
idom >=1.0.0a3, <1.1.0
2+
idom >=1.0.0a5, <1.1.0
33
aiofile >=3.0
44
dill >=0.3.5
55
typing_extensions

src/django_idom/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH
33

44

5-
__version__ = "3.0.0a2"
5+
__version__ = "3.0.0a3"
66
__all__ = [
77
"IDOM_WEBSOCKET_PATH",
88
"hooks",

src/django_idom/apps.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22

33
from django.apps import AppConfig
4-
from django.db.utils import OperationalError
4+
from django.db.utils import DatabaseError
55

66
from django_idom.utils import ComponentPreloader, db_cleanup
77

@@ -22,5 +22,7 @@ def ready(self):
2222
# where the database may not be ready.
2323
try:
2424
db_cleanup(immediate=True)
25-
except OperationalError:
26-
_logger.debug("IDOM database was not ready at startup. Skipping cleanup...")
25+
except DatabaseError:
26+
_logger.debug(
27+
"Could not access IDOM database at startup. Skipping cleanup..."
28+
)

src/django_idom/components.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any, Callable, Protocol, Sequence, Union, cast, overload
66

77
from django.contrib.staticfiles.finders import find
8+
from django.core.cache import caches
89
from django.http import HttpRequest
910
from django.urls import reverse
1011
from django.views import View
@@ -76,7 +77,10 @@ async def async_render():
7677
view, _args, _kwargs
7778
)
7879
return html.iframe(
79-
src=reverse("idom:view_to_component", args=[dotted_path]), loading="lazy"
80+
{
81+
"src": reverse("idom:view_to_component", args=[dotted_path]),
82+
"loading": "lazy",
83+
}
8084
)
8185

8286
# Return the view if it's been rendered via the `async_render` hook
@@ -209,12 +213,12 @@ def _cached_static_contents(static_path: str):
209213
# Cache is preferrable to `use_memo` due to multiprocessing capabilities
210214
last_modified_time = os.stat(abs_path).st_mtime
211215
cache_key = f"django_idom:static_contents:{static_path}"
212-
file_contents = IDOM_CACHE.get(cache_key, version=int(last_modified_time))
216+
file_contents = caches[IDOM_CACHE].get(cache_key, version=int(last_modified_time))
213217
if file_contents is None:
214218
with open(abs_path, encoding="utf-8") as static_file:
215219
file_contents = static_file.read()
216-
IDOM_CACHE.delete(cache_key)
217-
IDOM_CACHE.set(
220+
caches[IDOM_CACHE].delete(cache_key)
221+
caches[IDOM_CACHE].set(
218222
cache_key, file_contents, timeout=None, version=int(last_modified_time)
219223
)
220224

src/django_idom/config.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from typing import Dict
44

55
from django.conf import settings
6-
from django.core.cache import DEFAULT_CACHE_ALIAS, BaseCache, caches
6+
from django.core.cache import DEFAULT_CACHE_ALIAS
7+
from django.db import DEFAULT_DB_ALIAS
78
from idom.config import IDOM_DEBUG_MODE
89
from idom.core.types import ComponentConstructor
910

@@ -24,10 +25,15 @@
2425
"IDOM_RECONNECT_MAX",
2526
259200, # Default to 3 days
2627
)
27-
IDOM_CACHE: BaseCache = (
28-
caches["idom"]
29-
if "idom" in getattr(settings, "CACHES", {})
30-
else caches[DEFAULT_CACHE_ALIAS]
28+
IDOM_CACHE: str = getattr(
29+
settings,
30+
"IDOM_CACHE",
31+
DEFAULT_CACHE_ALIAS,
32+
)
33+
IDOM_DATABASE: str = getattr(
34+
settings,
35+
"IDOM_DATABASE",
36+
DEFAULT_DB_ALIAS,
3137
)
3238
IDOM_DEFAULT_QUERY_POSTPROCESSOR: Postprocessor | None = import_dotted_path(
3339
getattr(

src/django_idom/hooks.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
Awaitable,
88
Callable,
99
DefaultDict,
10-
MutableMapping,
1110
Sequence,
1211
Union,
1312
cast,
@@ -65,9 +64,14 @@ def use_origin() -> str | None:
6564
return None
6665

6766

68-
def use_scope() -> MutableMapping[str, Any]:
67+
def use_scope() -> dict[str, Any]:
6968
"""Get the current ASGI scope dictionary"""
70-
return _use_scope()
69+
scope = _use_scope()
70+
71+
if isinstance(scope, dict):
72+
return scope
73+
74+
raise TypeError(f"Expected scope to be a dict, got {type(scope)}")
7175

7276

7377
def use_connection() -> Connection:

src/django_idom/http/views.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22

33
from aiofile import async_open
4+
from django.core.cache import caches
45
from django.core.exceptions import SuspiciousOperation
56
from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
67
from idom.config import IDOM_WEB_MODULES_DIR
@@ -25,12 +26,12 @@ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
2526
# Fetch the file from cache, if available
2627
last_modified_time = os.stat(path).st_mtime
2728
cache_key = create_cache_key("web_module", str(path).lstrip(str(web_modules_dir)))
28-
response = await IDOM_CACHE.aget(cache_key, version=int(last_modified_time))
29+
response = await caches[IDOM_CACHE].aget(cache_key, version=int(last_modified_time))
2930
if response is None:
3031
async with async_open(path, "r") as fp:
3132
response = HttpResponse(await fp.read(), content_type="text/javascript")
32-
await IDOM_CACHE.adelete(cache_key)
33-
await IDOM_CACHE.aset(
33+
await caches[IDOM_CACHE].adelete(cache_key)
34+
await caches[IDOM_CACHE].aset(
3435
cache_key, response, timeout=None, version=int(last_modified_time)
3536
)
3637
return response
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 4.1.6 on 2023-02-21 10:59
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("django_idom", "0002_rename_created_at_componentparams_last_accessed"),
9+
]
10+
11+
operations = [
12+
migrations.CreateModel(
13+
name="ComponentSession",
14+
fields=[
15+
(
16+
"uuid",
17+
models.UUIDField(
18+
editable=False, primary_key=True, serialize=False, unique=True
19+
),
20+
),
21+
("params", models.BinaryField()),
22+
("last_accessed", models.DateTimeField(auto_now_add=True)),
23+
],
24+
),
25+
migrations.DeleteModel(
26+
name="ComponentParams",
27+
),
28+
]

src/django_idom/models.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from django.db import models
22

33

4-
class ComponentParams(models.Model):
4+
class ComponentSession(models.Model):
5+
"""A model for storing component parameters.
6+
All queries must be routed through `django_idom.config.IDOM_DATABASE`.
7+
"""
8+
59
uuid = models.UUIDField(primary_key=True, editable=False, unique=True) # type: ignore
6-
data = models.BinaryField(editable=False) # type: ignore
10+
params = models.BinaryField(editable=False) # type: ignore
711
last_accessed = models.DateTimeField(auto_now_add=True) # type: ignore

src/django_idom/templatetags/idom.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def component(dotted_path: str, *args, **kwargs):
4545
try:
4646
if func_has_params(component, *args, **kwargs):
4747
params = ComponentParamData(args, kwargs)
48-
model = models.ComponentParams(uuid=uuid, data=pickle.dumps(params))
48+
model = models.ComponentSession(uuid=uuid, params=pickle.dumps(params))
4949
model.full_clean()
5050
model.save()
5151
except TypeError as e:

src/django_idom/utils.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import Any, Callable, Sequence
1313

1414
from channels.db import database_sync_to_async
15+
from django.core.cache import caches
1516
from django.db.models import ManyToManyField, prefetch_related_objects
1617
from django.db.models.base import Model
1718
from django.db.models.fields.reverse_related import ManyToOneRel
@@ -311,20 +312,20 @@ def create_cache_key(*args):
311312
def db_cleanup(immediate: bool = False):
312313
"""Deletes expired component parameters from the database.
313314
This function may be expanded in the future to include additional cleanup tasks."""
314-
from .config import IDOM_CACHE, IDOM_RECONNECT_MAX
315-
from .models import ComponentParams
315+
from .config import IDOM_CACHE, IDOM_DATABASE, IDOM_RECONNECT_MAX
316+
from .models import ComponentSession
316317

317318
cache_key: str = create_cache_key("last_cleaned")
318319
now_str: str = datetime.strftime(timezone.now(), DATE_FORMAT)
319-
cleaned_at_str: str = IDOM_CACHE.get(cache_key)
320+
cleaned_at_str: str = caches[IDOM_CACHE].get(cache_key)
320321
cleaned_at: datetime = timezone.make_aware(
321322
datetime.strptime(cleaned_at_str or now_str, DATE_FORMAT)
322323
)
323324
clean_needed_by = cleaned_at + timedelta(seconds=IDOM_RECONNECT_MAX)
324325
expires_by: datetime = timezone.now() - timedelta(seconds=IDOM_RECONNECT_MAX)
325326

326327
# Component params exist in the DB, but we don't know when they were last cleaned
327-
if not cleaned_at_str and ComponentParams.objects.all():
328+
if not cleaned_at_str and ComponentSession.objects.using(IDOM_DATABASE).all():
328329
_logger.warning(
329330
"IDOM has detected component sessions in the database, "
330331
"but no timestamp was found in cache. This may indicate that "
@@ -334,5 +335,7 @@ def db_cleanup(immediate: bool = False):
334335
# Delete expired component parameters
335336
# Use timestamps in cache (`cleaned_at_str`) as a no-dependency rate limiter
336337
if immediate or not cleaned_at_str or timezone.now() >= clean_needed_by:
337-
ComponentParams.objects.filter(last_accessed__lte=expires_by).delete()
338-
IDOM_CACHE.set(cache_key, now_str)
338+
ComponentSession.objects.using(IDOM_DATABASE).filter(
339+
last_accessed__lte=expires_by
340+
).delete()
341+
caches[IDOM_CACHE].set(cache_key, now_str)

src/django_idom/websocket/consumer.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ async def receive_json(self, content: Any, **_) -> None:
5555

5656
async def _run_dispatch_loop(self):
5757
from django_idom import models
58-
from django_idom.config import IDOM_RECONNECT_MAX, IDOM_REGISTERED_COMPONENTS
58+
from django_idom.config import (
59+
IDOM_DATABASE,
60+
IDOM_RECONNECT_MAX,
61+
IDOM_REGISTERED_COMPONENTS,
62+
)
5963

6064
scope = self.scope
6165
dotted_path = scope["url_route"]["kwargs"]["dotted_path"]
@@ -91,20 +95,22 @@ async def _run_dispatch_loop(self):
9195
await convert_to_async(db_cleanup)()
9296

9397
# Get the queries from a DB
94-
params_query = await models.ComponentParams.objects.aget(
98+
params_query = await models.ComponentSession.objects.using(
99+
IDOM_DATABASE
100+
).aget(
95101
uuid=uuid,
96102
last_accessed__gt=now - timedelta(seconds=IDOM_RECONNECT_MAX),
97103
)
98104
params_query.last_accessed = timezone.now()
99105
await convert_to_async(params_query.save)()
100-
except models.ComponentParams.DoesNotExist:
106+
except models.ComponentSession.DoesNotExist:
101107
_logger.warning(
102108
f"Browser has attempted to access '{dotted_path}', "
103109
f"but the component has already expired beyond IDOM_RECONNECT_MAX. "
104110
"If this was expected, this warning can be ignored."
105111
)
106112
return
107-
component_params: ComponentParamData = pickle.loads(params_query.data)
113+
component_params: ComponentParamData = pickle.loads(params_query.params)
108114
component_args = component_params.args
109115
component_kwargs = component_params.kwargs
110116

0 commit comments

Comments
 (0)