djhtmx 1.1.1__py3-none-any.whl → 1.2.0__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.
- djhtmx/__init__.py +1 -1
- djhtmx/apps.py +4 -2
- djhtmx/component.py +3 -3
- djhtmx/introspection.py +5 -2
- djhtmx/management/commands/htmx.py +52 -0
- djhtmx/utils.py +27 -0
- {djhtmx-1.1.1.dist-info → djhtmx-1.2.0.dist-info}/METADATA +80 -1
- {djhtmx-1.1.1.dist-info → djhtmx-1.2.0.dist-info}/RECORD +10 -10
- {djhtmx-1.1.1.dist-info → djhtmx-1.2.0.dist-info}/WHEEL +0 -0
- {djhtmx-1.1.1.dist-info → djhtmx-1.2.0.dist-info}/licenses/LICENSE +0 -0
djhtmx/__init__.py
CHANGED
djhtmx/apps.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from django.apps import AppConfig
|
|
2
2
|
from django.utils.module_loading import autodiscover_modules
|
|
3
3
|
|
|
4
|
+
from .utils import autodiscover_htmx_modules
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
class App(AppConfig):
|
|
6
8
|
name = "djhtmx"
|
|
7
9
|
verbose_name = "Django HTMX"
|
|
8
10
|
|
|
9
11
|
def ready(self):
|
|
10
|
-
autodiscover_modules("live")
|
|
11
|
-
|
|
12
|
+
autodiscover_modules("live") # legacy
|
|
13
|
+
autodiscover_htmx_modules()
|
djhtmx/component.py
CHANGED
|
@@ -20,6 +20,7 @@ from typing import (
|
|
|
20
20
|
get_type_hints,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
23
24
|
from django.db import models
|
|
24
25
|
from django.shortcuts import resolve_url
|
|
25
26
|
from django.template import Context, loader
|
|
@@ -306,9 +307,8 @@ class HtmxComponent(BaseModel):
|
|
|
306
307
|
basename(cls._template_name.default)
|
|
307
308
|
not in (f"{klass.__name__}.html" for klass in cls.__mro__)
|
|
308
309
|
):
|
|
309
|
-
|
|
310
|
-
"HTMX Component
|
|
311
|
-
FQN[cls],
|
|
310
|
+
raise ImproperlyConfigured(
|
|
311
|
+
f"HTMX Component <{FQN[cls]}> template name does not match the component name"
|
|
312
312
|
)
|
|
313
313
|
|
|
314
314
|
# We use 'get_type_hints' to resolve the forward refs if needed, but
|
djhtmx/introspection.py
CHANGED
|
@@ -419,9 +419,12 @@ def is_simple_annotation(ann):
|
|
|
419
419
|
|
|
420
420
|
|
|
421
421
|
def is_collection_annotation(ann):
|
|
422
|
-
|
|
422
|
+
if isinstance(ann, types.GenericAlias):
|
|
423
|
+
return issubclass_safe(ann.__origin__, _COLLECTION_TYPES)
|
|
424
|
+
else:
|
|
425
|
+
return issubclass_safe(ann, _COLLECTION_TYPES)
|
|
423
426
|
|
|
424
427
|
|
|
425
428
|
Unset = object()
|
|
426
429
|
_SIMPLE_TYPES = (int, str, float, UUID, types.NoneType, date, datetime, bool)
|
|
427
|
-
_COLLECTION_TYPES = (dict, tuple, list, set)
|
|
430
|
+
_COLLECTION_TYPES = (dict, tuple, list, set, defaultdict)
|
|
@@ -46,6 +46,58 @@ def check_missing(fname):
|
|
|
46
46
|
sys.exit(1)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
@htmx.command("check-unused") # type: ignore
|
|
50
|
+
@click.argument("fname", type=click.File())
|
|
51
|
+
def check_unused(fname):
|
|
52
|
+
r"""Check if there are any unused HTMX components.
|
|
53
|
+
|
|
54
|
+
Expected usage:
|
|
55
|
+
|
|
56
|
+
find -type f -name '*.html' | while read f; do grep -P '{% htmx .(\\w+)' -o $f \
|
|
57
|
+
| awk '{print $3}' | cut -b2-; done | sort -u \
|
|
58
|
+
| python manage.py htmx check-unused -
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
names = {n.strip() for n in fname.readlines()}
|
|
62
|
+
known = set(REGISTRY)
|
|
63
|
+
unused = list(known - names)
|
|
64
|
+
if unused:
|
|
65
|
+
unused.sort()
|
|
66
|
+
for n in unused:
|
|
67
|
+
click.echo(
|
|
68
|
+
f"Unused component detected {bold(yellow(n))}",
|
|
69
|
+
file=sys.stderr,
|
|
70
|
+
)
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@htmx.command("check-unused-non-public") # type: ignore
|
|
75
|
+
def check_unused_non_public():
|
|
76
|
+
"""Check if there are any unused non-public HTMX components.
|
|
77
|
+
|
|
78
|
+
Non-public components that are final subclasses (have no subclasses themselves)
|
|
79
|
+
are considered unused since they can't be instantiated from templates and serve
|
|
80
|
+
no purpose as base classes.
|
|
81
|
+
"""
|
|
82
|
+
final_subclasses = set(
|
|
83
|
+
get_final_subclasses(
|
|
84
|
+
HtmxComponent, # type: ignore
|
|
85
|
+
without_duplicates=True,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
registered = set(REGISTRY.values())
|
|
89
|
+
unused_non_public = list(final_subclasses - registered)
|
|
90
|
+
|
|
91
|
+
if unused_non_public:
|
|
92
|
+
unused_non_public.sort(key=lambda cls: cls.__name__)
|
|
93
|
+
for cls in unused_non_public:
|
|
94
|
+
click.echo(
|
|
95
|
+
f"Unused non-public component detected {bold(yellow(cls.__name__))}",
|
|
96
|
+
file=sys.stderr,
|
|
97
|
+
)
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
|
|
49
101
|
@htmx.command("check-shadowing") # type: ignore
|
|
50
102
|
def check_shadowing():
|
|
51
103
|
"Checks if there are components that might shadow one another."
|
djhtmx/utils.py
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import importlib
|
|
3
|
+
import pkgutil
|
|
1
4
|
import typing as t
|
|
2
5
|
from urllib.parse import urlparse
|
|
3
6
|
|
|
4
7
|
import mmh3
|
|
5
8
|
from channels.db import database_sync_to_async as db # type: ignore
|
|
9
|
+
from django.apps import apps
|
|
6
10
|
from django.db import models
|
|
7
11
|
from django.http.request import HttpRequest, QueryDict
|
|
8
12
|
from uuid6 import uuid7
|
|
@@ -115,3 +119,26 @@ def compact_hash(value: str) -> str:
|
|
|
115
119
|
# The symbols are chosen to avoid extra encoding in the URL and HTML, and
|
|
116
120
|
# allowed in plain CSS selectors.
|
|
117
121
|
_BASE = "ZmBeUHhTgusXNW_Y1b05KPiFcQJD86joqnIRE7Lfkrdp3AOMCvltSwzVG9yxa42"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def autodiscover_htmx_modules():
|
|
125
|
+
"""
|
|
126
|
+
Auto-discover HTMX modules in Django apps.
|
|
127
|
+
|
|
128
|
+
This discovers both:
|
|
129
|
+
- htmx.py files (like standard autodiscover_modules("htmx"))
|
|
130
|
+
- All Python files under htmx/ directories in apps (recursively)
|
|
131
|
+
"""
|
|
132
|
+
def _import_modules_recursively(module_name):
|
|
133
|
+
"""Recursively import a module and all its submodules."""
|
|
134
|
+
with contextlib.suppress(ImportError):
|
|
135
|
+
module = importlib.import_module(module_name)
|
|
136
|
+
|
|
137
|
+
# If this is a package, recursively import all modules in it
|
|
138
|
+
if hasattr(module, "__path__"):
|
|
139
|
+
for _finder, submodule_name, _is_pkg in pkgutil.iter_modules(module.__path__):
|
|
140
|
+
_import_modules_recursively(f"{module_name}.{submodule_name}")
|
|
141
|
+
|
|
142
|
+
for app_config in apps.get_app_configs():
|
|
143
|
+
# Import htmx module and all its submodules recursively
|
|
144
|
+
_import_modules_recursively(f"{app_config.name}.htmx")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: djhtmx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Interactive UI Components for Django using HTMX
|
|
5
5
|
Project-URL: Homepage, https://github.com/edelvalle/djhtmx
|
|
6
6
|
Project-URL: Documentation, https://github.com/edelvalle/djhtmx#readme
|
|
@@ -66,6 +66,20 @@ pip install djhtmx
|
|
|
66
66
|
|
|
67
67
|
# Configuration
|
|
68
68
|
|
|
69
|
+
## Requirements
|
|
70
|
+
|
|
71
|
+
djhtmx requires **Redis** to be running for session storage and component state management.
|
|
72
|
+
|
|
73
|
+
**Important**: Redis is not included with djhtmx and must be installed separately on your system. Make sure Redis is installed and accessible before using djhtmx.
|
|
74
|
+
|
|
75
|
+
### Installing Redis
|
|
76
|
+
|
|
77
|
+
- **macOS**: `brew install redis`
|
|
78
|
+
- **Ubuntu/Debian**: `sudo apt-get install redis-server`
|
|
79
|
+
- **CentOS/RHEL**: `sudo yum install redis` or `sudo dnf install redis`
|
|
80
|
+
- **Docker**: `docker run -d -p 6379:6379 redis:alpine`
|
|
81
|
+
- **Windows**: Download from [Redis for Windows](https://github.com/microsoftarchive/redis/releases)
|
|
82
|
+
|
|
69
83
|
Add `djhtmx` to your `INSTALLED_APPS`.
|
|
70
84
|
|
|
71
85
|
```python
|
|
@@ -116,6 +130,40 @@ urlpatterns = [
|
|
|
116
130
|
]
|
|
117
131
|
```
|
|
118
132
|
|
|
133
|
+
## Settings
|
|
134
|
+
|
|
135
|
+
djhtmx can be configured through Django settings:
|
|
136
|
+
|
|
137
|
+
### Required Settings
|
|
138
|
+
|
|
139
|
+
- **`DJHTMX_REDIS_URL`** (default: `"redis://localhost/0"`): Redis connection URL for session storage and component state management.
|
|
140
|
+
|
|
141
|
+
### Optional Settings
|
|
142
|
+
|
|
143
|
+
- **`DJHTMX_SESSION_TTL`** (default: `3600`): Session timeout in seconds. Can be an integer or a `datetime.timedelta` object.
|
|
144
|
+
- **`DJHTMX_DEFAULT_LAZY_TEMPLATE`** (default: `"htmx/lazy.html"`): Default template for lazy-loaded components.
|
|
145
|
+
- **`DJHTMX_ENABLE_SENTRY_TRACING`** (default: `True`): Enable Sentry tracing integration.
|
|
146
|
+
- **`DJHTMX_ENABLE_LOGFIRE_TRACING`** (default: `False`): Enable Logfire tracing integration.
|
|
147
|
+
- **`DJHTMX_STRICT_EVENT_HANDLER_CONSISTENCY_CHECK`** (default: `False`): Enable strict consistency checking for event handlers.
|
|
148
|
+
- **`DJHTMX_KEY_SIZE_ERROR_THRESHOLD`** (default: `0`): Threshold in bytes for session key size errors (0 = disabled).
|
|
149
|
+
- **`DJHTMX_KEY_SIZE_WARN_THRESHOLD`** (default: `51200`): Threshold in bytes for session key size warnings (50KB).
|
|
150
|
+
- **`DJHTMX_KEY_SIZE_SAMPLE_PROB`** (default: `0.1`): Probability for sampling session key size checks.
|
|
151
|
+
|
|
152
|
+
### Example Configuration
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# settings.py
|
|
156
|
+
|
|
157
|
+
# Redis connection (required)
|
|
158
|
+
DJHTMX_REDIS_URL = "redis://localhost:6379/0" # or redis://user:password@host:port/db
|
|
159
|
+
|
|
160
|
+
# Optional settings
|
|
161
|
+
DJHTMX_SESSION_TTL = 7200 # 2 hours
|
|
162
|
+
DJHTMX_DEFAULT_LAZY_TEMPLATE = "my_app/lazy_component.html"
|
|
163
|
+
DJHTMX_ENABLE_SENTRY_TRACING = True
|
|
164
|
+
DJHTMX_KEY_SIZE_WARN_THRESHOLD = 100 * 1024 # 100KB
|
|
165
|
+
```
|
|
166
|
+
|
|
119
167
|
In your base template you need to load the necessary scripts to make this work
|
|
120
168
|
|
|
121
169
|
```html
|
|
@@ -130,8 +178,39 @@ In your base template you need to load the necessary scripts to make this work
|
|
|
130
178
|
|
|
131
179
|
## Getting started
|
|
132
180
|
|
|
181
|
+
**Important**: djhtmx is a framework for building interactive components, not a component library. No pre-built components, templates, or behaviors are provided. You need to create your own components from scratch using the framework's base classes and conventions.
|
|
182
|
+
|
|
183
|
+
This library is opinionated about how to use HTMX with Django, but it is not opinionated about components, styling, or specific functionality. You have complete freedom to design and implement your components as needed for your application.
|
|
184
|
+
|
|
133
185
|
This app will look for `htmx.py` files in your app and registers all components found there, but if you load any module where you have components manually when Django boots up, that also works.
|
|
134
186
|
|
|
187
|
+
### Component Organization
|
|
188
|
+
|
|
189
|
+
As of version 1.2.0, djhtmx supports both single file and directory-based component organization:
|
|
190
|
+
|
|
191
|
+
**Single file (traditional):**
|
|
192
|
+
```
|
|
193
|
+
myapp/
|
|
194
|
+
├── htmx.py # All components in one file
|
|
195
|
+
└── ...
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Directory structure (new in v1.2.0):**
|
|
199
|
+
```
|
|
200
|
+
myapp/
|
|
201
|
+
├── htmx/
|
|
202
|
+
│ ├── __init__.py
|
|
203
|
+
│ ├── components.py # Basic components
|
|
204
|
+
│ ├── forms.py # Form components
|
|
205
|
+
│ └── widgets/
|
|
206
|
+
│ ├── __init__.py
|
|
207
|
+
│ ├── calendar.py # Calendar widgets
|
|
208
|
+
│ └── charts.py # Chart widgets
|
|
209
|
+
└── ...
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The autodiscovery system will recursively find and import all Python modules under `htmx/` directories, allowing you to organize your components in a structured way that scales with your project size.
|
|
213
|
+
|
|
135
214
|
```python
|
|
136
215
|
from djhtmx.component import HtmxComponent
|
|
137
216
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
djhtmx/__init__.py,sha256=
|
|
2
|
-
djhtmx/apps.py,sha256=
|
|
1
|
+
djhtmx/__init__.py,sha256=3QdXDcoVk5icMta3uAvAdFpaUBzAyNy162DvVsELvRk,84
|
|
2
|
+
djhtmx/apps.py,sha256=hAyjzmInEstxLY9k8Qn58LvNlezgQLx5_NqyVL1WwYs,323
|
|
3
3
|
djhtmx/command_queue.py,sha256=kiYbQFPyjnhMSR7KgO1Nu-lWiapnH511P2Pyg-Zrdq4,4862
|
|
4
4
|
djhtmx/commands.py,sha256=UxXbARd4Teetjh_zjvAWgI2KNbvdETH-WrGf4qD9Xr8,1206
|
|
5
|
-
djhtmx/component.py,sha256=
|
|
5
|
+
djhtmx/component.py,sha256=0Z5gAbhOg7lKXzbwDGmVJVv_IA6WYtzrWxZo_ww9CpY,15789
|
|
6
6
|
djhtmx/consumer.py,sha256=kHNoXokcWaFjs5zbZAhM7Y0x7GVwwawXbxBCkP8HNA8,2839
|
|
7
7
|
djhtmx/context.py,sha256=cWvz8Z0MC6x_G8sn5mvoH8Hu38qReY21_eNdThuba1A,214
|
|
8
8
|
djhtmx/exceptions.py,sha256=UtyE1N-52OmzwgRM9xFxjUuhHTMDvD7Oy3rNpgthLcs,47
|
|
9
9
|
djhtmx/global_events.py,sha256=bYb8WmQn_WsZ_Dadr0pGiGOPia01K-VanPpM97Lt324,342
|
|
10
|
-
djhtmx/introspection.py,sha256=
|
|
10
|
+
djhtmx/introspection.py,sha256=OVNaQ8r6G13HMftydfW2UVCfs2dzy34HET2Tb7bGnN4,13431
|
|
11
11
|
djhtmx/json.py,sha256=7cjwWIJj7e0dk54INKYZJe6zKkIW7wlsNSlD05cbXfY,1374
|
|
12
12
|
djhtmx/middleware.py,sha256=JuMtv9ZnpungTvQ1qD2Lg6LiFPB3knQlA1ERgH4iGl0,1274
|
|
13
13
|
djhtmx/query.py,sha256=UyjN1jokh4wTwQJxcRwA9f-Zn-A7A4GLToeGrCnPhKA,6674
|
|
@@ -16,8 +16,8 @@ djhtmx/settings.py,sha256=ymFUMvrcXDkYU9KkhPOjRZSQMWz5GcUjlgh07x09-1s,1242
|
|
|
16
16
|
djhtmx/testing.py,sha256=AdZKsT6sNTsyqSKx6EmfthOIHzSAPkTquheMfg9znbk,8301
|
|
17
17
|
djhtmx/tracing.py,sha256=xkCXb7t_3yCj1PGzmQfHPu9sYQftDKwtALaEbFVnQ1E,1260
|
|
18
18
|
djhtmx/urls.py,sha256=zWMlw_udCUWvo5DNxsvbebSNRFxy0C9ghBmRg08XlcU,3894
|
|
19
|
-
djhtmx/utils.py,sha256=
|
|
20
|
-
djhtmx/management/commands/htmx.py,sha256=
|
|
19
|
+
djhtmx/utils.py,sha256=8dzSjEk4IR8LBHg-fDR9yn1Y2IVQjpmNn9x59rK5wgM,4645
|
|
20
|
+
djhtmx/management/commands/htmx.py,sha256=tEtiJn_Z6byOFzBNIzTbdluA4T5q21zFwGvJ7yt90bw,3642
|
|
21
21
|
djhtmx/static/htmx/django.js,sha256=G59uwy5hA4QUcAFJv21SMxizATpNZG3KfgFlO2zXeGc,7086
|
|
22
22
|
djhtmx/static/htmx/2.0.4/htmx.amd.js,sha256=Hgmm_X5zw7ek0pjBaxhzH7OHx6Xfce5UYVa9ICWlWR0,165593
|
|
23
23
|
djhtmx/static/htmx/2.0.4/htmx.cjs.js,sha256=4P3vh1eGwULBCT7wsKQ2bu4HiNQ_Kmnv2fP1RQ6_QW8,165586
|
|
@@ -30,7 +30,7 @@ djhtmx/templates/htmx/headers.html,sha256=rBQTBt9rnlxE8lgxN4U7nvzQZNw4JZKS4flD1V
|
|
|
30
30
|
djhtmx/templates/htmx/lazy.html,sha256=LfAThtKmFj-lCUZ7JWF_sC1Y6XsIpEz8A3IgWASn-J8,52
|
|
31
31
|
djhtmx/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
32
|
djhtmx/templatetags/htmx.py,sha256=HlH7_B9lJoTDoIkYPeEE55OwpBTrrCesE70j1KcRC70,8063
|
|
33
|
-
djhtmx-1.
|
|
34
|
-
djhtmx-1.
|
|
35
|
-
djhtmx-1.
|
|
36
|
-
djhtmx-1.
|
|
33
|
+
djhtmx-1.2.0.dist-info/METADATA,sha256=YxslHSLGP3ZdSVy0gYuJZ8xLluzIce0Ne38lfpneCi8,32245
|
|
34
|
+
djhtmx-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
35
|
+
djhtmx-1.2.0.dist-info/licenses/LICENSE,sha256=kCi_iSBUGsRZInQn96w7LXYzjiRjZ8FXl6vP--mFRPk,1085
|
|
36
|
+
djhtmx-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|