django-workspaces 0.0.1a1__py3-none-any.whl

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.
@@ -0,0 +1,80 @@
1
+ """Django reusable app to manage user workspaces.
2
+
3
+ 'Workspace' in this package is a unit of work. Each session always has one
4
+ active workspace. Anything can be added to a workspace.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING, Any, cast
8
+
9
+ import django_stubs_ext
10
+ from django.apps import apps as django_apps
11
+ from django.conf import settings
12
+ from django.http import Http404, HttpRequest
13
+ from django.shortcuts import aget_object_or_404, get_object_or_404
14
+
15
+ from .signals import workspace_requested
16
+ from .types import _Workspace, _WorkspaceModel
17
+
18
+ if TYPE_CHECKING:
19
+ from django.contrib.auth.models import AbstractUser, AnonymousUser
20
+
21
+ django_stubs_ext.monkeypatch()
22
+
23
+ __all__ = [
24
+ "aget_workspace",
25
+ "get_workspace",
26
+ "get_workspace_model",
27
+ "workspace_requested",
28
+ ]
29
+
30
+ SESSION_KEY = "_workspace_id"
31
+
32
+
33
+ def get_workspace_model() -> _WorkspaceModel:
34
+ """Return the workspace model that is active for this project.
35
+
36
+ The workspace model defaults to :class:`django_workspaces.models.Workspace`, and can
37
+ be swapped through the ``WORKSPACE_MODEL`` setting.
38
+ """
39
+ workspace_model_name: str = getattr(settings, "WORKSPACE_MODEL", "django_workspaces.Workspace")
40
+ return django_apps.get_model(workspace_model_name, require_ready=False)
41
+
42
+
43
+ def get_workspace(request: HttpRequest) -> _Workspace:
44
+ """Return the workspace model instance associated with the given request."""
45
+ Workspace: _WorkspaceModel = get_workspace_model() # noqa: N806
46
+ user: AbstractUser | AnonymousUser = request.user
47
+
48
+ try:
49
+ workspace_id = Workspace._meta.pk.to_python(request.session[SESSION_KEY]) # noqa: SLF001
50
+ except KeyError as exc:
51
+ responses = workspace_requested.send(Workspace, user=user, request=request)
52
+ if not responses:
53
+ msg = "Could not find a workspace"
54
+ raise Http404(msg) from exc
55
+
56
+ _, workspace = cast("tuple[Any, _Workspace]", responses[0])
57
+ else:
58
+ workspace = get_object_or_404(Workspace, pk=workspace_id)
59
+
60
+ return workspace
61
+
62
+
63
+ async def aget_workspace(request: HttpRequest) -> _Workspace:
64
+ """Async version of :func:`get_workspace`."""
65
+ Workspace: _WorkspaceModel = get_workspace_model() # noqa: N806
66
+ user: AbstractUser | AnonymousUser = await request.auser()
67
+
68
+ session_workspace = await request.session.aget(SESSION_KEY)
69
+ if session_workspace is None:
70
+ responses = await workspace_requested.asend(Workspace, user=user, request=request)
71
+ if not responses:
72
+ msg = "Could not find a workspace"
73
+ raise Http404(msg)
74
+
75
+ _, workspace = cast("tuple[Any, _Workspace]", responses[0])
76
+ else:
77
+ workspace_id = Workspace._meta.pk.to_python(session_workspace) # noqa: SLF001
78
+ workspace = await aget_object_or_404(Workspace, pk=workspace_id)
79
+
80
+ return workspace
@@ -0,0 +1,17 @@
1
+ import sys
2
+
3
+ if sys.version_info >= (3, 12):
4
+ from inspect import iscoroutinefunction, markcoroutinefunction
5
+ else:
6
+ from asgiref.sync import iscoroutinefunction, markcoroutinefunction
7
+
8
+ if sys.version_info >= (3, 13):
9
+ from typing import TypeIs
10
+ else:
11
+ from typing_extensions import TypeIs
12
+
13
+ __all__ = [
14
+ "TypeIs",
15
+ "iscoroutinefunction",
16
+ "markcoroutinefunction",
17
+ ]
@@ -0,0 +1,12 @@
1
+ """Read by Django to configure :mod:`django_workspaces`."""
2
+
3
+ from django.apps import AppConfig
4
+ from django.utils.translation import gettext_lazy as _
5
+
6
+
7
+ class WorkspacesConfig(AppConfig):
8
+ """:mod:`django_workspaces` app configuration."""
9
+
10
+ default_auto_field = "django.db.models.BigAutoField"
11
+ name = "django_workspaces"
12
+ verbose_name = _("Workspaces")
@@ -0,0 +1,39 @@
1
+ """Workspace middlewares."""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from functools import partial
5
+ from typing import cast
6
+
7
+ from django.core.exceptions import ImproperlyConfigured
8
+ from django.http import HttpResponseBase
9
+ from django.utils.decorators import sync_and_async_middleware
10
+ from django.utils.functional import SimpleLazyObject
11
+
12
+ from . import aget_workspace, get_workspace
13
+ from ._compat import iscoroutinefunction, markcoroutinefunction
14
+ from .types import HttpRequest, _Workspace
15
+
16
+ _Middleware = Callable[[HttpRequest], HttpResponseBase] | Callable[[HttpRequest], Awaitable[HttpResponseBase]]
17
+
18
+
19
+ @sync_and_async_middleware
20
+ def workspace_middleware(get_response: _Middleware, /) -> _Middleware:
21
+ """Django middleware to add the current workspace to every request.
22
+
23
+ Adds the property `workspace` to use in sync contexts, and the
24
+ `aworkspace` corourine function for async contexts.
25
+ """
26
+
27
+ def middleware(request: HttpRequest) -> HttpResponseBase:
28
+ if not hasattr(request, "user"):
29
+ msg: str = "The workspace middleware requires Django's authentication middleware"
30
+ raise ImproperlyConfigured(msg)
31
+
32
+ request.workspace = cast("_Workspace", SimpleLazyObject(partial(get_workspace, request)))
33
+ request.aworkspace = partial(aget_workspace, request)
34
+ return get_response(request) # type: ignore[return-value]
35
+
36
+ if iscoroutinefunction(get_response):
37
+ middleware = markcoroutinefunction(middleware)
38
+
39
+ return middleware
@@ -0,0 +1,29 @@
1
+ from django.db import migrations, models
2
+
3
+
4
+ class Migration(migrations.Migration):
5
+ initial = True
6
+
7
+ dependencies = []
8
+
9
+ operations = [
10
+ migrations.CreateModel(
11
+ name="Workspace",
12
+ fields=[
13
+ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
14
+ (
15
+ "name",
16
+ models.CharField(
17
+ db_comment="Workspace name",
18
+ help_text="Required. 255 characters or fewer.",
19
+ max_length=255,
20
+ verbose_name="name",
21
+ ),
22
+ ),
23
+ ],
24
+ options={
25
+ "abstract": False,
26
+ "swappable": "WORKSPACE_MODEL",
27
+ },
28
+ ),
29
+ ]
File without changes
@@ -0,0 +1,33 @@
1
+ """Workspace models."""
2
+
3
+ from django.db import models
4
+ from django.utils.translation import gettext_lazy as _
5
+ from django_stubs_ext.db.models import TypedModelMeta
6
+
7
+
8
+ class AbstractWorkspace(models.Model):
9
+ """Abstract base class implementing a workspace.
10
+
11
+ Custom workspace models should inherit from this class.
12
+ """
13
+
14
+ name = models.CharField(
15
+ _("name"),
16
+ max_length=255,
17
+ help_text=_("Required. 255 characters or fewer."),
18
+ db_comment="Workspace name",
19
+ )
20
+
21
+ class Meta(TypedModelMeta):
22
+ abstract = True
23
+
24
+ def __str__(self) -> str:
25
+ """Return a string representation of the workspace."""
26
+ return self.name
27
+
28
+
29
+ class Workspace(AbstractWorkspace):
30
+ """Default workspace model."""
31
+
32
+ class Meta(AbstractWorkspace.Meta):
33
+ swappable = "WORKSPACE_MODEL"
@@ -0,0 +1 @@
1
+ PEP-561 marker file
@@ -0,0 +1,17 @@
1
+ """Dispatched signals."""
2
+
3
+ from django.dispatch import Signal
4
+
5
+ workspace_requested = Signal()
6
+ """Dispatched when a workspace is not found in the current session.
7
+
8
+ This should look up the user's preferences to get the latest or default workspace.
9
+
10
+ Args:
11
+ sender: The current workspace model.
12
+ user: The user requesting a workspace.
13
+ request: The current request. Optional.
14
+
15
+ Returns:
16
+ A workspace instance if could find a default workspace. None otherwise.
17
+ """
@@ -0,0 +1,23 @@
1
+ """Type helpers."""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from typing import TYPE_CHECKING, TypeAlias
5
+
6
+ from django.http import HttpRequest as DjangoHttpRequest
7
+
8
+ if TYPE_CHECKING:
9
+ from .models import AbstractWorkspace
10
+
11
+ _Workspace: TypeAlias = "AbstractWorkspace"
12
+ """Placeholder type for the current workspace.
13
+
14
+ The mypy plugin will refine it someday."""
15
+
16
+ _WorkspaceModel: TypeAlias = type[_Workspace] # noqa: PYI047
17
+
18
+
19
+ class HttpRequest(DjangoHttpRequest):
20
+ """HTTP request with workspace."""
21
+
22
+ workspace: _Workspace
23
+ aworkspace: Callable[[], Awaitable[_Workspace]]
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-workspaces
3
+ Version: 0.0.1a1
4
+ Summary: Django reusable app to manage user workspaces
5
+ Project-URL: Documentation, https://github.com/hartungstenio/django-workspaces#readme
6
+ Project-URL: Issues, https://github.com/hartungstenio/django-workspaces/issues
7
+ Project-URL: Source, https://github.com/hartungstenio/django-workspaces
8
+ Author-email: Christian Hartung <hartung@live.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE.txt
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Framework :: Django
14
+ Classifier: Framework :: Django :: 5.2
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: Implementation :: CPython
24
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Requires-Dist: django
29
+ Requires-Dist: django-stubs-ext
30
+ Requires-Dist: typing-extensions; python_version < '3.13'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # django-workspaces
34
+
35
+ [![PyPI - Version](https://img.shields.io/pypi/v/django-workspaces.svg)](https://pypi.org/project/django-workspaces)
36
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-workspaces.svg)](https://pypi.org/project/django-workspaces)
37
+
38
+ -----
39
+
40
+ ## Table of Contents
41
+
42
+ - [Installation](#installation)
43
+ - [License](#license)
44
+
45
+ ## Installation
46
+
47
+ ```console
48
+ pip install django-workspaces
49
+ ```
50
+
51
+ ## License
52
+
53
+ `django-workspaces` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
@@ -0,0 +1,14 @@
1
+ django_workspaces/__init__.py,sha256=hp5hUNZij9nR2iukGqbZAyxWMZxG2w3LCRxyfvNPG9o,2806
2
+ django_workspaces/_compat.py,sha256=vLgdGvaSApBvHJZ3JK7S-dRk1v_TcFf81zyxHzE6NIE,384
3
+ django_workspaces/apps.py,sha256=z6wVAXJjYHEg2UpyfDZY-YciGc4T46VxcBxHKXyRNkQ,365
4
+ django_workspaces/middleware.py,sha256=-RSe99WOUk2fs32znJ9fuUdFGY-ZZFdzNWreub3WwHg,1504
5
+ django_workspaces/models.py,sha256=EALgPW4gGw6CMMKqzagbzWLPO4I06-u6MLXa6BL_ymM,829
6
+ django_workspaces/py.typed,sha256=Qn1PtAFCQCHxVY6FV1vsRH98Re39dTxwl3lQRksOGpY,20
7
+ django_workspaces/signals.py,sha256=aspua_A38H-QSym7qiZ5SOE7jB50SY3S3m2N4Tzxj5w,471
8
+ django_workspaces/types.py,sha256=JawPN4lIPKDERS4pm83z44sy7ea2MmOUFJbjAwQQ4YA,589
9
+ django_workspaces/migrations/0001_initial.py,sha256=FKwAb9uyrYVhBi6PznmG9skoXLV2bwO0SKw-mUUGfVA,833
10
+ django_workspaces/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ django_workspaces-0.0.1a1.dist-info/METADATA,sha256=A--DX4jj1QKAc6ssZXfYW0fRHVbUXNLG8QXUkE_6DD8,1930
12
+ django_workspaces-0.0.1a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ django_workspaces-0.0.1a1.dist-info/licenses/LICENSE.txt,sha256=kw90zQM3KTw6c7cenDnJY8bGSv6MM46DpIXHiU9g_h8,1101
14
+ django_workspaces-0.0.1a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Christian Hartung <hartung@live.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.