Skip to content

Commit

Permalink
3.0.0a3 (#127)
Browse files Browse the repository at this point in the history
- 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.*`.
  • Loading branch information
Archmonger authored Feb 21, 2023
1 parent 25b2c12 commit 0e37352
Show file tree
Hide file tree
Showing 19 changed files with 202 additions and 112 deletions.
24 changes: 13 additions & 11 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Using the following categories, list your changes in this order:

- Nothing (yet)

## [3.0.0a2] - 2023-02-02
## [3.0.0a3] - 2023-02-21

???+ note

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

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

### Added

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

### Changed

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

### Removed

- `django_idom.hooks.use_websocket` has been removed. The similar replacement is `django_idom.hooks.use_connection`.
- `django_idom.types.IdomWebsocket` has been removed. The similar replacement is `django_idom.types.Connection`
- `django_idom.types.IdomWebsocket` has been removed. The similar replacement is `django_idom.types.Connection`.
- `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.

### Fixed

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

### Security

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

- Support for IDOM within the Django

[unreleased]: https://github.com/idom-team/django-idom/compare/3.0.0a2...HEAD
[3.0.0a2]: https://github.com/idom-team/django-idom/compare/2.2.1...3.0.0a2
[unreleased]: https://github.com/idom-team/django-idom/compare/3.0.0a3...HEAD
[3.0.0a3]: https://github.com/idom-team/django-idom/compare/2.2.1...3.0.0a3
[2.2.1]: https://github.com/idom-team/django-idom/compare/2.2.0...2.2.1
[2.2.0]: https://github.com/idom-team/django-idom/compare/2.1.0...2.2.0
[2.1.0]: https://github.com/idom-team/django-idom/compare/2.0.1...2.1.0
Expand Down
12 changes: 6 additions & 6 deletions docs/python/settings.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# If "idom" cache is not configured, then "default" will be used
# IDOM works best with a multiprocessing-safe and thread-safe cache backend.
CACHES = {
"idom": {"BACKEND": ...},
}
# IDOM requires a multiprocessing-safe and thread-safe cache.
IDOM_CACHE = "default"

# IDOM requires a multiprocessing-safe and thread-safe database.
IDOM_DATABASE = "default"

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

# Dotted path to the default postprocessor function, or `None`
# Dotted path to the default `django_idom.hooks.use_query` postprocessor function, or `None`
IDOM_DEFAULT_QUERY_POSTPROCESSOR = "example_project.utils.my_postprocessor"
2 changes: 1 addition & 1 deletion requirements/pkg-deps.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
channels >=4.0.0
idom >=1.0.0a3, <1.1.0
idom >=1.0.0a5, <1.1.0
aiofile >=3.0
dill >=0.3.5
typing_extensions
2 changes: 1 addition & 1 deletion src/django_idom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH


__version__ = "3.0.0a2"
__version__ = "3.0.0a3"
__all__ = [
"IDOM_WEBSOCKET_PATH",
"hooks",
Expand Down
8 changes: 5 additions & 3 deletions src/django_idom/apps.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging

from django.apps import AppConfig
from django.db.utils import OperationalError
from django.db.utils import DatabaseError

from django_idom.utils import ComponentPreloader, db_cleanup

Expand All @@ -22,5 +22,7 @@ def ready(self):
# where the database may not be ready.
try:
db_cleanup(immediate=True)
except OperationalError:
_logger.debug("IDOM database was not ready at startup. Skipping cleanup...")
except DatabaseError:
_logger.debug(
"Could not access IDOM database at startup. Skipping cleanup..."
)
12 changes: 8 additions & 4 deletions src/django_idom/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any, Callable, Protocol, Sequence, Union, cast, overload

from django.contrib.staticfiles.finders import find
from django.core.cache import caches
from django.http import HttpRequest
from django.urls import reverse
from django.views import View
Expand Down Expand Up @@ -76,7 +77,10 @@ async def async_render():
view, _args, _kwargs
)
return html.iframe(
src=reverse("idom:view_to_component", args=[dotted_path]), loading="lazy"
{
"src": reverse("idom:view_to_component", args=[dotted_path]),
"loading": "lazy",
}
)

# Return the view if it's been rendered via the `async_render` hook
Expand Down Expand Up @@ -209,12 +213,12 @@ def _cached_static_contents(static_path: str):
# Cache is preferrable to `use_memo` due to multiprocessing capabilities
last_modified_time = os.stat(abs_path).st_mtime
cache_key = f"django_idom:static_contents:{static_path}"
file_contents = IDOM_CACHE.get(cache_key, version=int(last_modified_time))
file_contents = caches[IDOM_CACHE].get(cache_key, version=int(last_modified_time))
if file_contents is None:
with open(abs_path, encoding="utf-8") as static_file:
file_contents = static_file.read()
IDOM_CACHE.delete(cache_key)
IDOM_CACHE.set(
caches[IDOM_CACHE].delete(cache_key)
caches[IDOM_CACHE].set(
cache_key, file_contents, timeout=None, version=int(last_modified_time)
)

Expand Down
16 changes: 11 additions & 5 deletions src/django_idom/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from typing import Dict

from django.conf import settings
from django.core.cache import DEFAULT_CACHE_ALIAS, BaseCache, caches
from django.core.cache import DEFAULT_CACHE_ALIAS
from django.db import DEFAULT_DB_ALIAS
from idom.config import IDOM_DEBUG_MODE
from idom.core.types import ComponentConstructor

Expand All @@ -24,10 +25,15 @@
"IDOM_RECONNECT_MAX",
259200, # Default to 3 days
)
IDOM_CACHE: BaseCache = (
caches["idom"]
if "idom" in getattr(settings, "CACHES", {})
else caches[DEFAULT_CACHE_ALIAS]
IDOM_CACHE: str = getattr(
settings,
"IDOM_CACHE",
DEFAULT_CACHE_ALIAS,
)
IDOM_DATABASE: str = getattr(
settings,
"IDOM_DATABASE",
DEFAULT_DB_ALIAS,
)
IDOM_DEFAULT_QUERY_POSTPROCESSOR: Postprocessor | None = import_dotted_path(
getattr(
Expand Down
10 changes: 7 additions & 3 deletions src/django_idom/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
Awaitable,
Callable,
DefaultDict,
MutableMapping,
Sequence,
Union,
cast,
Expand Down Expand Up @@ -65,9 +64,14 @@ def use_origin() -> str | None:
return None


def use_scope() -> MutableMapping[str, Any]:
def use_scope() -> dict[str, Any]:
"""Get the current ASGI scope dictionary"""
return _use_scope()
scope = _use_scope()

if isinstance(scope, dict):
return scope

raise TypeError(f"Expected scope to be a dict, got {type(scope)}")


def use_connection() -> Connection:
Expand Down
7 changes: 4 additions & 3 deletions src/django_idom/http/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os

from aiofile import async_open
from django.core.cache import caches
from django.core.exceptions import SuspiciousOperation
from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
from idom.config import IDOM_WEB_MODULES_DIR
Expand All @@ -25,12 +26,12 @@ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
# Fetch the file from cache, if available
last_modified_time = os.stat(path).st_mtime
cache_key = create_cache_key("web_module", str(path).lstrip(str(web_modules_dir)))
response = await IDOM_CACHE.aget(cache_key, version=int(last_modified_time))
response = await caches[IDOM_CACHE].aget(cache_key, version=int(last_modified_time))
if response is None:
async with async_open(path, "r") as fp:
response = HttpResponse(await fp.read(), content_type="text/javascript")
await IDOM_CACHE.adelete(cache_key)
await IDOM_CACHE.aset(
await caches[IDOM_CACHE].adelete(cache_key)
await caches[IDOM_CACHE].aset(
cache_key, response, timeout=None, version=int(last_modified_time)
)
return response
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.1.6 on 2023-02-21 10:59

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("django_idom", "0002_rename_created_at_componentparams_last_accessed"),
]

operations = [
migrations.CreateModel(
name="ComponentSession",
fields=[
(
"uuid",
models.UUIDField(
editable=False, primary_key=True, serialize=False, unique=True
),
),
("params", models.BinaryField()),
("last_accessed", models.DateTimeField(auto_now_add=True)),
],
),
migrations.DeleteModel(
name="ComponentParams",
),
]
8 changes: 6 additions & 2 deletions src/django_idom/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from django.db import models


class ComponentParams(models.Model):
class ComponentSession(models.Model):
"""A model for storing component parameters.
All queries must be routed through `django_idom.config.IDOM_DATABASE`.
"""

uuid = models.UUIDField(primary_key=True, editable=False, unique=True) # type: ignore
data = models.BinaryField(editable=False) # type: ignore
params = models.BinaryField(editable=False) # type: ignore
last_accessed = models.DateTimeField(auto_now_add=True) # type: ignore
2 changes: 1 addition & 1 deletion src/django_idom/templatetags/idom.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def component(dotted_path: str, *args, **kwargs):
try:
if func_has_params(component, *args, **kwargs):
params = ComponentParamData(args, kwargs)
model = models.ComponentParams(uuid=uuid, data=pickle.dumps(params))
model = models.ComponentSession(uuid=uuid, params=pickle.dumps(params))
model.full_clean()
model.save()
except TypeError as e:
Expand Down
15 changes: 9 additions & 6 deletions src/django_idom/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import Any, Callable, Sequence

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

cache_key: str = create_cache_key("last_cleaned")
now_str: str = datetime.strftime(timezone.now(), DATE_FORMAT)
cleaned_at_str: str = IDOM_CACHE.get(cache_key)
cleaned_at_str: str = caches[IDOM_CACHE].get(cache_key)
cleaned_at: datetime = timezone.make_aware(
datetime.strptime(cleaned_at_str or now_str, DATE_FORMAT)
)
clean_needed_by = cleaned_at + timedelta(seconds=IDOM_RECONNECT_MAX)
expires_by: datetime = timezone.now() - timedelta(seconds=IDOM_RECONNECT_MAX)

# Component params exist in the DB, but we don't know when they were last cleaned
if not cleaned_at_str and ComponentParams.objects.all():
if not cleaned_at_str and ComponentSession.objects.using(IDOM_DATABASE).all():
_logger.warning(
"IDOM has detected component sessions in the database, "
"but no timestamp was found in cache. This may indicate that "
Expand All @@ -334,5 +335,7 @@ def db_cleanup(immediate: bool = False):
# Delete expired component parameters
# Use timestamps in cache (`cleaned_at_str`) as a no-dependency rate limiter
if immediate or not cleaned_at_str or timezone.now() >= clean_needed_by:
ComponentParams.objects.filter(last_accessed__lte=expires_by).delete()
IDOM_CACHE.set(cache_key, now_str)
ComponentSession.objects.using(IDOM_DATABASE).filter(
last_accessed__lte=expires_by
).delete()
caches[IDOM_CACHE].set(cache_key, now_str)
14 changes: 10 additions & 4 deletions src/django_idom/websocket/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ async def receive_json(self, content: Any, **_) -> None:

async def _run_dispatch_loop(self):
from django_idom import models
from django_idom.config import IDOM_RECONNECT_MAX, IDOM_REGISTERED_COMPONENTS
from django_idom.config import (
IDOM_DATABASE,
IDOM_RECONNECT_MAX,
IDOM_REGISTERED_COMPONENTS,
)

scope = self.scope
dotted_path = scope["url_route"]["kwargs"]["dotted_path"]
Expand Down Expand Up @@ -91,20 +95,22 @@ async def _run_dispatch_loop(self):
await convert_to_async(db_cleanup)()

# Get the queries from a DB
params_query = await models.ComponentParams.objects.aget(
params_query = await models.ComponentSession.objects.using(
IDOM_DATABASE
).aget(
uuid=uuid,
last_accessed__gt=now - timedelta(seconds=IDOM_RECONNECT_MAX),
)
params_query.last_accessed = timezone.now()
await convert_to_async(params_query.save)()
except models.ComponentParams.DoesNotExist:
except models.ComponentSession.DoesNotExist:
_logger.warning(
f"Browser has attempted to access '{dotted_path}', "
f"but the component has already expired beyond IDOM_RECONNECT_MAX. "
"If this was expected, this warning can be ignored."
)
return
component_params: ComponentParamData = pickle.loads(params_query.data)
component_params: ComponentParamData = pickle.loads(params_query.params)
component_args = component_params.args
component_kwargs = component_params.kwargs

Expand Down
Loading

0 comments on commit 0e37352

Please sign in to comment.