Skip to content

Async ORM mutations and queries #134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 50 commits into from
May 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e76ca9c
set thread_sensitive=false on safe locations
Archmonger Mar 15, 2023
2504a71
delete unneeded file
Archmonger Mar 15, 2023
06eedcf
remove useless variable rename
Archmonger Mar 15, 2023
6d82574
fully async `use_mutation` and `use_query`
Archmonger Mar 15, 2023
5092ead
add debug print statements
Archmonger Mar 16, 2023
aae9f5c
fix type hints
Archmonger Mar 16, 2023
a79d99b
schedule_query
Archmonger Mar 16, 2023
95afac1
fix types
Archmonger Mar 16, 2023
aa0a459
remove unneeded ignore
Archmonger Mar 16, 2023
8add17a
separate async query models
Archmonger Mar 17, 2023
1384493
add tests
Archmonger Mar 17, 2023
143baa1
add thread_sensitive parameter
Archmonger Mar 17, 2023
0c1485c
pretend to fix type hints
Archmonger Mar 17, 2023
03a9202
add documentation
Archmonger Mar 17, 2023
5d007db
add async use_query docs
Archmonger Mar 17, 2023
5aabe0c
rename mutate to mutation
Archmonger Mar 17, 2023
e4a262e
`MutationOptions`
Archmonger Mar 18, 2023
2ada981
update changelog
Archmonger Mar 18, 2023
b8b666c
dry use_mutation_args
Archmonger Mar 18, 2023
730047e
reduce LOCs by 1
Archmonger Mar 18, 2023
b4fd339
add MutationOptions to types.__all__
Archmonger Mar 18, 2023
55106ae
fix test_async_relational_query
Archmonger Mar 18, 2023
b77ab2d
legacy docstring cleanup
Archmonger Mar 18, 2023
c895484
turn off thread_sensitive everywhere middleware is not used
Archmonger Mar 18, 2023
0f59ab2
fix typo
Archmonger Mar 18, 2023
fe34724
fix typo
Archmonger Mar 19, 2023
70aae1a
add register view page
Archmonger Mar 25, 2023
f8cdb93
refactor hooks
Archmonger Mar 25, 2023
b1e1d27
change docs section titles
Archmonger Mar 25, 2023
87df7b3
comment cleanup
Archmonger Mar 25, 2023
07f4134
Allow `query=...` to be defined as a kwarg
Archmonger Mar 26, 2023
dbb785a
Merge remote-tracking branch 'upstream/main' into thread-sensitive
Archmonger Mar 31, 2023
0b5f747
increase test db timeout
Archmonger Mar 31, 2023
c239668
Merge remote-tracking branch 'upstream/main' into thread-sensitive
Archmonger Apr 2, 2023
f67fce5
Merge remote-tracking branch 'upstream/main' into thread-sensitive
Archmonger Apr 13, 2023
2024900
test cleanup
Archmonger Apr 14, 2023
99c6969
faster test runs
Archmonger Apr 14, 2023
d4ccffb
better routing
Archmonger Apr 14, 2023
5544d18
fix `manage.py test` when initial DB does not exist
Archmonger Apr 14, 2023
b95736b
fix changelog date
Archmonger Apr 15, 2023
762dcd6
Tests for database routing
Archmonger Apr 15, 2023
063250a
use django 4.2 API for tests
Archmonger Apr 15, 2023
abbfda0
run all tests in 11 seconds
Archmonger Apr 15, 2023
785ed60
re-enable headless
Archmonger Apr 15, 2023
d2ec1d8
increase test sleep
Archmonger Apr 15, 2023
c16d16a
decrease timeout
Archmonger Apr 15, 2023
867415f
change jupyter link
Archmonger Apr 16, 2023
4d9bda6
private args functions
Archmonger May 6, 2023
15bc111
compatibility with the latest django stubs
Archmonger May 6, 2023
e307512
bump changelog
Archmonger May 6, 2023
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ logs
*.pyc
.dccachea
__pycache__
db.sqlite3
*.sqlite3
*.sqlite3-journal
media
cache
static-deploy
Expand Down
17 changes: 15 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,22 @@ Using the following categories, list your changes in this order:

## [Unreleased]

- Nothing (yet)!
### Added

- `use_query` now supports async functions.
- `use_mutation` now supports async functions.
- `reactpy_django.types.QueryOptions.thread_sensitive` option to customize how sync queries are executed.
- `reactpy_django.hooks.use_mutation` now accepts `reactpy_django.types.MutationOptions` option to customize how mutations are executed.

### Changed

- The `mutate` argument on `reactpy_django.hooks.use_mutation` has been renamed to `mutation`.

### Fixed

- Fix bug where ReactPy utilizes Django's default cache timeout, which can prematurely expire the component cache.

## [3.0.1] - 2023-03-31
## [3.0.1] - 2023-04-06

### Changed

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</td>
<td>
<a href="https://github.com/reactive-python/reactpy-django">Django</a>,
<a href="https://github.com/idom-team/idom-jupyter">Jupyter</a>,
<a href="https://github.com/reactive-python/reactpy-jupyter">Jupyter</a>,
<a href="https://github.com/idom-team/idom-dash">Plotly-Dash</a>
</td>
</tr>
Expand Down
23 changes: 23 additions & 0 deletions docs/python/use-query-async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from channels.db import database_sync_to_async
from example.models import TodoItem
from reactpy import component, html

from reactpy_django.hooks import use_query


async def get_items():
return await database_sync_to_async(TodoItem.objects.all)()


@component
def todo_list():
item_query = use_query(get_items)

if item_query.loading:
rendered_items = html.h2("Loading...")
elif item_query.error or not item_query.data:
rendered_items = html.h2("Error when loading!")
else:
rendered_items = html.ul([html.li(item, key=item) for item in item_query.data])

return html.div("Rendered items: ", rendered_items)
2 changes: 1 addition & 1 deletion docs/python/use-query-postprocessor-change.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def execute_io_intensive_operation():


@component
def todo_list():
def my_component():
query = use_query(
QueryOptions(
postprocessor=my_postprocessor,
Expand Down
2 changes: 1 addition & 1 deletion docs/python/use-query-postprocessor-disable.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def execute_io_intensive_operation():


@component
def todo_list():
def my_component():
query = use_query(
QueryOptions(postprocessor=None),
execute_io_intensive_operation,
Expand Down
2 changes: 1 addition & 1 deletion docs/python/use-query-postprocessor-kwargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def get_model_with_relationships():


@component
def todo_list():
def my_component():
query = use_query(
QueryOptions(
postprocessor_kwargs={"many_to_many": False, "many_to_one": False}
Expand Down
22 changes: 22 additions & 0 deletions docs/python/use-query-thread-sensitive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from reactpy import component

from reactpy_django.hooks import use_query
from reactpy_django.types import QueryOptions


def execute_thread_safe_operation():
"""This is an example query function that does some thread-safe operation."""
pass


@component
def my_component():
query = use_query(
QueryOptions(thread_sensitive=False),
execute_thread_safe_operation,
)

if query.loading or query.error:
return None

return str(query.data)
2 changes: 1 addition & 1 deletion docs/python/use-query.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ def todo_list():
elif item_query.error or not item_query.data:
rendered_items = html.h2("Error when loading!")
else:
rendered_items = html.ul(html.li(item, key=item) for item in item_query.data)
rendered_items = html.ul([html.li(item, key=item) for item in item_query.data])

return html.div("Rendered items: ", rendered_items)
1 change: 1 addition & 0 deletions docs/src/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ postprocessing
serializable
postprocessor
preprocessor
middleware
backends
backend
frontend
Expand Down
38 changes: 34 additions & 4 deletions docs/src/features/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,29 @@ The function you provide into this hook must return either a `Model` or `QuerySe
{% include "../../python/use-query-args.py" %}
```

??? question "Why does the example `get_items` function return `TodoItem.objects.all()`?"
??? question "Why does `get_items` in the example return `TodoItem.objects.all()`?"

This was a technical design decision to based on [Apollo's `useQuery` hook](https://www.apollographql.com/docs/react/data/queries/), but ultimately helps avoid Django's `SynchronousOnlyOperation` exceptions.

The `use_query` hook ensures the provided `Model` or `QuerySet` executes all [deferred](https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.get_deferred_fields)/[lazy queries](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy) safely prior to reaching your components.

??? question "Can this hook be used for things other than the Django ORM?"
??? question "How can I use `QueryOptions` to customize fetching behavior?"

<font size="4">**`thread_sensitive`**</font>

Whether to run your synchronous query function in thread-sensitive mode. Thread-sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information.

This setting only applies to sync query functions, and will be ignored for async functions.

=== "components.py"

```python
{% include "../../python/use-query-thread-sensitive.py" %}
```

---

<font size="4">**`postprocessor`**</font>

{% include-markdown "../../includes/orm.md" start="<!--orm-fetch-start-->" end="<!--orm-fetch-end-->" %}

Expand All @@ -72,7 +88,7 @@ The function you provide into this hook must return either a `Model` or `QuerySe
1. Want to use this hook to defer IO intensive tasks to be computed in the background
2. Want to to utilize `use_query` with a different ORM

... then you can disable all postprocessing behavior by modifying the `QueryOptions.postprocessor` parameter. In the example below, we will set the `postprocessor` to `None` to disable postprocessing behavior.
... then you can either set a custom `postprocessor`, or disable all postprocessing behavior by modifying the `QueryOptions.postprocessor` parameter. In the example below, we will set the `postprocessor` to `None` to disable postprocessing behavior.

=== "components.py"

Expand All @@ -92,7 +108,9 @@ The function you provide into this hook must return either a `Model` or `QuerySe
{% include "../../python/use-query-postprocessor-change.py" %}
```

??? question "How can I prevent this hook from recursively fetching `ManyToMany` fields or `ForeignKey` relationships?"
---

<font size="4">**`postprocessor_kwargs`**</font>

{% include-markdown "../../includes/orm.md" start="<!--orm-fetch-start-->" end="<!--orm-fetch-end-->" %}

Expand All @@ -108,6 +126,18 @@ The function you provide into this hook must return either a `Model` or `QuerySe

_Note: In Django's ORM design, the field name to access foreign keys is [postfixed with `_set`](https://docs.djangoproject.com/en/dev/topics/db/examples/many_to_one/) by default._

??? question "Can I define async query functions?"

Async functions are supported by `use_query`. You can use them in the same way as a sync query function.

However, be mindful of Django async ORM restrictions.

=== "components.py"

```python
{% include "../../python/use-query-async.py" %}
```

??? question "Can I make ORM calls without hooks?"

{% include-markdown "../../includes/orm.md" start="<!--orm-excp-start-->" end="<!--orm-excp-end-->" %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

---

## Render Your View
## Register a View

We will assume you have [created a Django View](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) before, but here's a simple example below.

Expand All @@ -28,8 +28,6 @@ We will add this new view into your [`urls.py`](https://docs.djangoproject.com/e
{% include "../../python/example/urls.py" %}
```

Now, navigate to `http://127.0.0.1:8000/example/`. If you copy-pasted the component from the previous example, you will now see your component display "Hello World".

??? question "Which urls.py do I add my views to?"

For simple **Django projects**, you can easily add all of your views directly into the **Django project's** `urls.py`. However, as you start increase your project's complexity you might end up with way too much within one file.
Expand Down
23 changes: 23 additions & 0 deletions docs/src/get-started/run-webserver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Overview

!!! summary

Run a webserver to display your Django view.

---

## Run the Webserver

To test your new Django view, run the following command to start up a development webserver.

```bash linenums="0"
python manage.py runserver
```

Now you can navigate to your **Django project** URL that contains an ReactPy component, such as `http://127.0.0.1:8000/example/` (_from the previous step_).

If you copy-pasted our example component, you will now see your component display "Hello World".

??? warning "Do not use `manage.py runserver` for production."

The webserver contained within `manage.py runserver` is only intended for development and testing purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/dev/howto/deployment/).
3 changes: 2 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ nav:
- Choose a Django App: get-started/choose-django-app.md
- Create a Component: get-started/create-component.md
- Use the Template Tag: get-started/use-template-tag.md
- Render Your View: get-started/render-view.md
- Register a View: get-started/register-view.md
- Run the Webserver: get-started/run-webserver.md
- Learn More: get-started/learn-more.md
- Reference:
- Components: features/components.md
Expand Down
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test_suite(session: Session) -> None:
posargs.append("--debug-mode")

session.run("playwright", "install", "chromium")
session.run("python", "manage.py", "test", *posargs)
session.run("python", "manage.py", "test", *posargs, "-v 2")


@nox.session
Expand Down
1 change: 1 addition & 0 deletions requirements/test-env.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ django
playwright
twisted
channels[daphne]>=4.0.0
tblib
18 changes: 12 additions & 6 deletions src/reactpy_django/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
from reactpy.config import REACTPY_DEBUG_MODE
from reactpy.core.types import ComponentConstructor

from reactpy_django.types import Postprocessor, ViewComponentIframe
from reactpy_django.types import (
AsyncPostprocessor,
SyncPostprocessor,
ViewComponentIframe,
)
from reactpy_django.utils import import_dotted_path


Expand Down Expand Up @@ -37,10 +41,12 @@
"REACTPY_DATABASE",
DEFAULT_DB_ALIAS,
)
REACTPY_DEFAULT_QUERY_POSTPROCESSOR: Postprocessor | None = import_dotted_path(
getattr(
settings,
"REACTPY_DEFAULT_QUERY_POSTPROCESSOR",
"reactpy_django.utils.django_query_postprocessor",
REACTPY_DEFAULT_QUERY_POSTPROCESSOR: AsyncPostprocessor | SyncPostprocessor | None = (
import_dotted_path(
getattr(
settings,
"REACTPY_DEFAULT_QUERY_POSTPROCESSOR",
"reactpy_django.utils.django_query_postprocessor",
)
)
)
Loading