django-tailwind-cli 4.3.0__tar.gz → 4.4.0__tar.gz
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.
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.github/workflows/release.yml +7 -7
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.github/workflows/test.yml +2 -2
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.pre-commit-config.yaml +5 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/CHANGELOG.md +12 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/CLAUDE.md +33 -3
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/PKG-INFO +3 -3
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/README.md +1 -1
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/settings.md +2 -2
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/usage.md +2 -2
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/pyproject.toml +5 -14
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/config.py +11 -8
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/management/commands/tailwind.py +17 -44
- django_tailwind_cli-4.4.0/src/django_tailwind_cli/utils/__init__.py +1 -0
- django_tailwind_cli-4.4.0/src/django_tailwind_cli/utils/http.py +178 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_additional_commands.py +9 -1
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_config.py +11 -12
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_error_scenarios.py +85 -82
- django_tailwind_cli-4.4.0/tests/test_http.py +148 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_integration.py +191 -96
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_management_commands.py +46 -5
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tox.ini +11 -6
- django_tailwind_cli-4.4.0/uv.lock +507 -0
- django_tailwind_cli-4.3.0/uv.lock +0 -693
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.github/dependabot.yml +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.gitignore +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.readthedocs.yml +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/CODE_OF_CONDUCT.md +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/CONFIGURATION.md +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/DEVELOPMENT.md +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/LICENSE +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/base_template.md +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/changelog.md +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/index.md +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/installation.md +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/requirements.txt +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/template_tags.md +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/justfile +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/mkdocs.yml +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/__init__.py +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/apps.py +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/management/__init__.py +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/management/commands/__init__.py +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/py.typed +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/templates/tailwind_cli/base.html +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/templates/tailwind_cli/tailwind_css.html +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/templatetags/__init__.py +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/templatetags/tailwind_cli.py +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/.gitignore +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/__init__.py +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/assets/css/.gitkeep +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/assets/css/tailwind.css +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/conftest.py +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/settings.py +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/templates/tests/base.html +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/templates/tests/dummy.email +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_get_runserver_options.py +0 -0
- {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_tailwind_css_tag.py +0 -0
|
@@ -14,9 +14,9 @@ jobs:
|
|
|
14
14
|
runs-on: ubuntu-latest
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@v5
|
|
18
18
|
- name: Set up Python
|
|
19
|
-
uses: actions/setup-python@
|
|
19
|
+
uses: actions/setup-python@v6
|
|
20
20
|
with:
|
|
21
21
|
python-version: "3.x"
|
|
22
22
|
- name: Install pypa/build
|
|
@@ -44,12 +44,12 @@ jobs:
|
|
|
44
44
|
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
45
45
|
steps:
|
|
46
46
|
- name: Download all the dists
|
|
47
|
-
uses: actions/download-artifact@
|
|
47
|
+
uses: actions/download-artifact@v5
|
|
48
48
|
with:
|
|
49
49
|
name: python-package-distributions
|
|
50
50
|
path: dist/
|
|
51
51
|
- name: Publish distribution 📦 to PyPI
|
|
52
|
-
uses: pypa/gh-action-pypi-publish@release/v1.
|
|
52
|
+
uses: pypa/gh-action-pypi-publish@release/v1.13
|
|
53
53
|
|
|
54
54
|
github-release:
|
|
55
55
|
name: >-
|
|
@@ -65,7 +65,7 @@ jobs:
|
|
|
65
65
|
|
|
66
66
|
steps:
|
|
67
67
|
- name: Download all the dists
|
|
68
|
-
uses: actions/download-artifact@
|
|
68
|
+
uses: actions/download-artifact@v5
|
|
69
69
|
with:
|
|
70
70
|
name: python-package-distributions
|
|
71
71
|
path: dist/
|
|
@@ -110,12 +110,12 @@ jobs:
|
|
|
110
110
|
|
|
111
111
|
steps:
|
|
112
112
|
- name: Download all the dists
|
|
113
|
-
uses: actions/download-artifact@
|
|
113
|
+
uses: actions/download-artifact@v5
|
|
114
114
|
with:
|
|
115
115
|
name: python-package-distributions
|
|
116
116
|
path: dist/
|
|
117
117
|
- name: Publish distribution 📦 to TestPyPI
|
|
118
|
-
uses: pypa/gh-action-pypi-publish@release/v1.
|
|
118
|
+
uses: pypa/gh-action-pypi-publish@release/v1.13
|
|
119
119
|
with:
|
|
120
120
|
repository-url: https://test.pypi.org/legacy/
|
|
121
121
|
skip-existing: true
|
|
@@ -14,10 +14,10 @@ jobs:
|
|
|
14
14
|
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@v5
|
|
18
18
|
|
|
19
19
|
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
-
uses: actions/setup-python@
|
|
20
|
+
uses: actions/setup-python@v6
|
|
21
21
|
with:
|
|
22
22
|
python-version: ${{ matrix.python-version }}
|
|
23
23
|
allow-prereleases: true
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 4.4.0 (2025-09-21)
|
|
6
|
+
|
|
7
|
+
### 🔧 Technical Improvements
|
|
8
|
+
- **Type safety**: Fixed all pyright typing errors for better code quality and maintainability
|
|
9
|
+
- **Code cleanup**: Removed unused functions and improved type annotations throughout codebase
|
|
10
|
+
- **Dependencies**: Removed requests dependency, replaced with custom HTTP implementation
|
|
11
|
+
- **Exception handling**: Fixed exception naming to follow Python conventions and avoid builtin shadowing
|
|
12
|
+
- **Test coverage**: Added comprehensive HTTP module tests, improving coverage from 82% to 85%
|
|
13
|
+
- **Error handling**: Complete test coverage for network timeouts, connection errors, and edge cases
|
|
14
|
+
- **VS Code integration**: Added PyLance ignore comments for test files accessing private methods
|
|
15
|
+
- **Django 6.0 support**: Added Django 6.0 to testing matrix and version compatibility
|
|
16
|
+
|
|
5
17
|
## 4.3.0 (2025-07-12)
|
|
6
18
|
|
|
7
19
|
### 🎯 New Features
|
|
@@ -114,11 +114,17 @@ Key Django settings for configuration:
|
|
|
114
114
|
## Version Support
|
|
115
115
|
|
|
116
116
|
- Python: 3.10-3.14
|
|
117
|
-
- Django: 4.0-
|
|
117
|
+
- Django: 4.0-6.0
|
|
118
118
|
- Tailwind CSS: 4.x only (use v2.21.1 for Tailwind 3.x)
|
|
119
119
|
|
|
120
120
|
## Commit Message Guidelines
|
|
121
121
|
|
|
122
|
+
**⚠️ CRITICAL REQUIREMENT: NO CLAUDE REFERENCES**
|
|
123
|
+
- **NEVER include any Claude Code references, marketing info, or AI-generated footers in commit messages**
|
|
124
|
+
- **NEVER add "Generated with Claude Code", "Co-Authored-By: Claude", or any similar lines**
|
|
125
|
+
- **Commit messages must be completely clean and professional - no AI tool attribution whatsoever**
|
|
126
|
+
- **This is a strict project requirement and violations will require commit message rewrites**
|
|
127
|
+
|
|
122
128
|
Use conventional commit format with the following structure:
|
|
123
129
|
|
|
124
130
|
```
|
|
@@ -176,7 +182,31 @@ chore(deps): bump django-typer to 2.1.2
|
|
|
176
182
|
```
|
|
177
183
|
|
|
178
184
|
### Important Notes
|
|
179
|
-
- **NEVER include Claude Code references in commit messages** - This is strictly prohibited
|
|
180
|
-
- **NEVER add "Generated with Claude Code" or "Co-Authored-By: Claude" lines** - Commit messages must be clean
|
|
181
185
|
- Keep commit messages focused on the technical changes made
|
|
182
186
|
- Use bullet points to describe key modifications and their impact
|
|
187
|
+
- Follow conventional commit format consistently
|
|
188
|
+
- Be concise but descriptive about what changed and why
|
|
189
|
+
|
|
190
|
+
## Changelog Requirements
|
|
191
|
+
|
|
192
|
+
**IMPORTANT**: Every time you are asked to commit changes, you MUST also update the CHANGELOG.md file:
|
|
193
|
+
|
|
194
|
+
1. **Update CHANGELOG.md**: Add a compact entry under the "## Unreleased" section describing the changes
|
|
195
|
+
2. **Format**: Use the existing changelog format with appropriate categories:
|
|
196
|
+
- 🎯 **New Features**: For new functionality
|
|
197
|
+
- ⚡ **Performance Improvements**: For optimization changes
|
|
198
|
+
- 🛠️ **Developer Experience**: For DX improvements
|
|
199
|
+
- 🔧 **Technical Improvements**: For internal code improvements
|
|
200
|
+
- 🐛 **Bug Fixes**: For fixes
|
|
201
|
+
- 📚 **Documentation**: For docs changes
|
|
202
|
+
3. **Keep it concise**: One or two bullet points maximum describing the key change
|
|
203
|
+
4. **Focus on user impact**: Describe what changed from a user's perspective, not implementation details
|
|
204
|
+
|
|
205
|
+
### Example Changelog Entry
|
|
206
|
+
```markdown
|
|
207
|
+
## Unreleased
|
|
208
|
+
|
|
209
|
+
### 🔧 Technical Improvements
|
|
210
|
+
- **Type safety**: Fixed all pyright typing errors for better code quality
|
|
211
|
+
- **Code cleanup**: Removed unused functions and improved type annotations
|
|
212
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-tailwind-cli
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
4
4
|
Summary: Django and Tailwind integration based on the prebuilt Tailwind CSS CLI.
|
|
5
5
|
Project-URL: Home, https://django-tailwind-cli.rtfd.io/
|
|
6
6
|
Project-URL: Documentation, https://django-tailwind-cli.rtfd.io/
|
|
@@ -34,6 +34,7 @@ Classifier: Framework :: Django :: 4.2
|
|
|
34
34
|
Classifier: Framework :: Django :: 5.0
|
|
35
35
|
Classifier: Framework :: Django :: 5.1
|
|
36
36
|
Classifier: Framework :: Django :: 5.2
|
|
37
|
+
Classifier: Framework :: Django :: 6.0
|
|
37
38
|
Classifier: Intended Audience :: Developers
|
|
38
39
|
Classifier: License :: OSI Approved :: MIT License
|
|
39
40
|
Classifier: Operating System :: OS Independent
|
|
@@ -47,7 +48,6 @@ Classifier: Topic :: Utilities
|
|
|
47
48
|
Requires-Python: >=3.10
|
|
48
49
|
Requires-Dist: django-typer>=2.1.2
|
|
49
50
|
Requires-Dist: django>=4.0
|
|
50
|
-
Requires-Dist: requests>=2.32.3
|
|
51
51
|
Requires-Dist: semver>=3.0.4
|
|
52
52
|
Provides-Extra: django-extensions
|
|
53
53
|
Requires-Dist: django-extensions>=3.2; extra == 'django-extensions'
|
|
@@ -216,7 +216,7 @@ Start adding Tailwind classes to your templates:
|
|
|
216
216
|
## 📋 Requirements
|
|
217
217
|
|
|
218
218
|
- **Python:** 3.10+
|
|
219
|
-
- **Django:** 4.0+
|
|
219
|
+
- **Django:** 4.0-6.0+
|
|
220
220
|
- **Platform:** Windows, macOS, Linux (automatic platform detection)
|
|
221
221
|
|
|
222
222
|
## ⚙️ Configuration Examples
|
|
@@ -160,7 +160,7 @@ Start adding Tailwind classes to your templates:
|
|
|
160
160
|
## 📋 Requirements
|
|
161
161
|
|
|
162
162
|
- **Python:** 3.10+
|
|
163
|
-
- **Django:** 4.0+
|
|
163
|
+
- **Django:** 4.0-6.0+
|
|
164
164
|
- **Platform:** Windows, macOS, Linux (automatic platform detection)
|
|
165
165
|
|
|
166
166
|
## ⚙️ Configuration Examples
|
|
@@ -66,13 +66,13 @@ your project.
|
|
|
66
66
|
> ```
|
|
67
67
|
|
|
68
68
|
`TAILWIND_CLI_SRC_CSS`
|
|
69
|
-
**Default**:
|
|
69
|
+
: **Default**: `".django_tailwind_cli/source.css"`
|
|
70
70
|
|
|
71
71
|
This variable can be set to a relative path and an absolute path.
|
|
72
72
|
|
|
73
73
|
If it is a relative path it is assumed to be relative to `settings.BASE_DIR`. If `settings.BASE_DIR` is not defined or the file doesn't exist a `ValueError` is raised.
|
|
74
74
|
|
|
75
|
-
If it is an absolute path, this path is used as the input file for Tailwind CSS CLI. If the path doesn't exist, a `ValueError`is raised.
|
|
75
|
+
If it is an absolute path, this path is used as the input file for Tailwind CSS CLI. If the path doesn't exist, a `ValueError` is raised.
|
|
76
76
|
|
|
77
77
|
`TAILWIND_CLI_DIST_CSS`
|
|
78
78
|
: **Default**: `"css/tailwind.css"`
|
|
@@ -89,7 +89,7 @@ Run `python manage.py tailwind config` to show current Tailwind CSS configuratio
|
|
|
89
89
|
|
|
90
90
|
The command shows:
|
|
91
91
|
- All configuration paths (CLI, CSS input/output)
|
|
92
|
-
- Version information
|
|
92
|
+
- Version information
|
|
93
93
|
- Django settings values
|
|
94
94
|
- File existence status
|
|
95
95
|
- Platform information
|
|
@@ -100,7 +100,7 @@ Run `python manage.py tailwind setup` to launch the interactive setup guide for
|
|
|
100
100
|
|
|
101
101
|
The guide covers:
|
|
102
102
|
1. Installation verification
|
|
103
|
-
2. Django settings configuration
|
|
103
|
+
2. Django settings configuration
|
|
104
104
|
3. CLI binary download
|
|
105
105
|
4. First CSS build
|
|
106
106
|
5. Template integration
|
|
@@ -24,15 +24,11 @@ classifiers = [
|
|
|
24
24
|
"Framework :: Django :: 5.0",
|
|
25
25
|
"Framework :: Django :: 5.1",
|
|
26
26
|
"Framework :: Django :: 5.2",
|
|
27
|
+
"Framework :: Django :: 6.0",
|
|
27
28
|
]
|
|
28
29
|
dynamic = ["version"]
|
|
29
30
|
requires-python = ">=3.10"
|
|
30
|
-
dependencies = [
|
|
31
|
-
"django>=4.0",
|
|
32
|
-
"django-typer>=2.1.2",
|
|
33
|
-
"requests>=2.32.3",
|
|
34
|
-
"semver>=3.0.4",
|
|
35
|
-
]
|
|
31
|
+
dependencies = ["django>=4.0", "django-typer>=2.1.2", "semver>=3.0.4"]
|
|
36
32
|
|
|
37
33
|
[project.optional-dependencies]
|
|
38
34
|
django-extensions = ["django-extensions>=3.2", "werkzeug>=3.0"]
|
|
@@ -50,7 +46,6 @@ dev = [
|
|
|
50
46
|
"pytest-randomly>=3.15.0",
|
|
51
47
|
"pytest-timeout>=2.3.1",
|
|
52
48
|
"pytest>=8.3.3",
|
|
53
|
-
"django-stubs[compatible-mypy]>=5.1.1",
|
|
54
49
|
]
|
|
55
50
|
|
|
56
51
|
[build-system]
|
|
@@ -66,15 +61,11 @@ pythonVersion = "3.10"
|
|
|
66
61
|
typeCheckingMode = "strict"
|
|
67
62
|
venvPath = ".venv"
|
|
68
63
|
venv = "."
|
|
64
|
+
reportPrivateUsage = "warning"
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
python_version = "3.10"
|
|
73
|
-
plugins = ["mypy_django_plugin.main"]
|
|
66
|
+
[tool.pyright.defineConstant]
|
|
67
|
+
TYPE_CHECKING = true
|
|
74
68
|
|
|
75
|
-
[tool.django-stubs]
|
|
76
|
-
django_settings_module = "tests.settings"
|
|
77
|
-
strict_settings = false
|
|
78
69
|
|
|
79
70
|
# Ruff
|
|
80
71
|
[tool.ruff]
|
|
@@ -78,7 +78,7 @@ from dataclasses import dataclass
|
|
|
78
78
|
from pathlib import Path
|
|
79
79
|
from typing import NamedTuple
|
|
80
80
|
|
|
81
|
-
import
|
|
81
|
+
from django_tailwind_cli.utils import http
|
|
82
82
|
from django.conf import settings
|
|
83
83
|
from semver import Version
|
|
84
84
|
|
|
@@ -175,7 +175,7 @@ def _validate_required_settings() -> None:
|
|
|
175
175
|
)
|
|
176
176
|
|
|
177
177
|
|
|
178
|
-
def
|
|
178
|
+
def get_platform_info() -> PlatformInfo:
|
|
179
179
|
"""Get platform information for CLI binary selection.
|
|
180
180
|
|
|
181
181
|
Returns:
|
|
@@ -288,13 +288,15 @@ def get_version() -> tuple[str, Version]:
|
|
|
288
288
|
# Fetch latest version from GitHub
|
|
289
289
|
timeout = getattr(settings, "TAILWIND_CLI_REQUEST_TIMEOUT", 10)
|
|
290
290
|
try:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
success, location = http.fetch_redirect_location(
|
|
292
|
+
f"https://github.com/{repo_url}/releases/latest/", timeout=timeout
|
|
293
|
+
)
|
|
294
|
+
if success and location:
|
|
295
|
+
version_str = location.rstrip("/").split("/")[-1].replace("v", "")
|
|
294
296
|
# Cache the result
|
|
295
297
|
_save_cached_version(repo_url, version_str)
|
|
296
298
|
return version_str, Version.parse(version_str)
|
|
297
|
-
except (
|
|
299
|
+
except (http.RequestError, ValueError):
|
|
298
300
|
# Network or parsing error, fall back to cached or default
|
|
299
301
|
pass
|
|
300
302
|
|
|
@@ -354,7 +356,8 @@ def _resolve_css_paths() -> tuple[Path, str, Path, bool]:
|
|
|
354
356
|
"TAILWIND_CLI_DIST_CSS must not be None. Either remove the setting or provide a valid CSS path."
|
|
355
357
|
)
|
|
356
358
|
|
|
357
|
-
first_staticfile_dir = settings.STATICFILES_DIRS[0]
|
|
359
|
+
first_staticfile_dir: str | tuple[str, str] = settings.STATICFILES_DIRS[0]
|
|
360
|
+
staticfile_path: str
|
|
358
361
|
if isinstance(first_staticfile_dir, tuple):
|
|
359
362
|
# Handle prefixed staticfile dir
|
|
360
363
|
staticfile_path = first_staticfile_dir[1]
|
|
@@ -425,7 +428,7 @@ def get_config() -> Config:
|
|
|
425
428
|
automatic_download = getattr(settings, "TAILWIND_CLI_AUTOMATIC_DOWNLOAD", True)
|
|
426
429
|
|
|
427
430
|
# Get platform information
|
|
428
|
-
platform_info =
|
|
431
|
+
platform_info = get_platform_info()
|
|
429
432
|
|
|
430
433
|
# Get version information
|
|
431
434
|
version_str, version = get_version()
|
|
@@ -12,7 +12,7 @@ from types import FrameType
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
from collections.abc import Callable
|
|
14
14
|
|
|
15
|
-
import
|
|
15
|
+
from django_tailwind_cli.utils import http
|
|
16
16
|
import typer
|
|
17
17
|
from django.conf import settings
|
|
18
18
|
from django.core.management.base import CommandError
|
|
@@ -21,7 +21,7 @@ from django_typer.management import Typer
|
|
|
21
21
|
|
|
22
22
|
from django_tailwind_cli.config import get_config
|
|
23
23
|
|
|
24
|
-
app = Typer(
|
|
24
|
+
app = Typer( # pyright: ignore[reportUnknownVariableType]
|
|
25
25
|
name="tailwind",
|
|
26
26
|
help="""Tailwind CSS integration for Django projects.
|
|
27
27
|
|
|
@@ -430,10 +430,11 @@ def list_templates(
|
|
|
430
430
|
_list_template_files_enhanced(app_template_dir, "app")
|
|
431
431
|
|
|
432
432
|
# Scan global template directories
|
|
433
|
-
global_template_dirs = settings.TEMPLATES[0]["DIRS"] if settings.TEMPLATES else []
|
|
433
|
+
global_template_dirs: list[str] = settings.TEMPLATES[0]["DIRS"] if settings.TEMPLATES else []
|
|
434
434
|
if verbose:
|
|
435
435
|
typer.secho(f"🌐 Found {len(global_template_dirs)} global template directories", fg=typer.colors.BLUE)
|
|
436
436
|
|
|
437
|
+
template_dir: str
|
|
437
438
|
for template_dir in global_template_dirs:
|
|
438
439
|
_list_template_files_enhanced(template_dir, "global")
|
|
439
440
|
|
|
@@ -579,9 +580,9 @@ def show_config():
|
|
|
579
580
|
typer.secho(f" TAILWIND_CLI_DIST_CSS: {dist_css_setting}", fg=typer.colors.GREEN)
|
|
580
581
|
|
|
581
582
|
# Platform information
|
|
582
|
-
from django_tailwind_cli.config import
|
|
583
|
+
from django_tailwind_cli.config import get_platform_info
|
|
583
584
|
|
|
584
|
-
platform_info =
|
|
585
|
+
platform_info = get_platform_info()
|
|
585
586
|
typer.secho("\n💻 Platform Information:", fg=typer.colors.YELLOW, bold=True)
|
|
586
587
|
typer.secho(f" Operating System: {platform_info.system}", fg=typer.colors.GREEN)
|
|
587
588
|
typer.secho(f" Architecture: {platform_info.machine}", fg=typer.colors.GREEN)
|
|
@@ -1283,47 +1284,24 @@ def _download_cli_with_progress(url: str, filepath: Path) -> None:
|
|
|
1283
1284
|
url: Download URL.
|
|
1284
1285
|
filepath: Destination file path.
|
|
1285
1286
|
"""
|
|
1286
|
-
|
|
1287
|
-
response = requests.get(url, stream=True, timeout=30)
|
|
1288
|
-
response.raise_for_status()
|
|
1289
|
-
|
|
1290
|
-
total_size = int(response.headers.get("content-length", 0))
|
|
1291
|
-
|
|
1292
|
-
if total_size == 0:
|
|
1293
|
-
# Fallback for unknown size
|
|
1294
|
-
typer.secho("Downloading Tailwind CSS CLI...", fg=typer.colors.YELLOW)
|
|
1295
|
-
filepath.write_bytes(response.content)
|
|
1296
|
-
return
|
|
1297
|
-
|
|
1298
|
-
# Download with progress
|
|
1299
|
-
downloaded = 0
|
|
1300
|
-
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
1301
|
-
|
|
1302
|
-
with filepath.open("wb") as f:
|
|
1303
|
-
typer.secho("Downloading Tailwind CSS CLI...", fg=typer.colors.YELLOW)
|
|
1304
|
-
for chunk in response.iter_content(chunk_size=8192):
|
|
1305
|
-
if chunk:
|
|
1306
|
-
f.write(chunk)
|
|
1307
|
-
downloaded += len(chunk)
|
|
1287
|
+
last_progress = 0
|
|
1308
1288
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1289
|
+
def progress_callback(downloaded: int, total_size: int, progress: float) -> None:
|
|
1290
|
+
nonlocal last_progress
|
|
1291
|
+
# Show progress every 10%
|
|
1292
|
+
if total_size > 0 and int(progress / 10) > int(last_progress / 10):
|
|
1293
|
+
typer.secho(f"Progress: {progress:.1f}% ({downloaded}/{total_size} bytes)", fg=typer.colors.CYAN)
|
|
1294
|
+
last_progress = progress
|
|
1314
1295
|
|
|
1296
|
+
try:
|
|
1297
|
+
typer.secho("Downloading Tailwind CSS CLI...", fg=typer.colors.YELLOW)
|
|
1298
|
+
http.download_with_progress(url, filepath, timeout=30, progress_callback=progress_callback)
|
|
1315
1299
|
typer.secho("Download completed!", fg=typer.colors.GREEN)
|
|
1316
1300
|
|
|
1317
|
-
except
|
|
1301
|
+
except http.RequestError as e:
|
|
1318
1302
|
raise CommandError(f"Failed to download Tailwind CSS CLI: {e}") from e
|
|
1319
1303
|
|
|
1320
1304
|
|
|
1321
|
-
def _setup_tailwind_environment() -> None:
|
|
1322
|
-
"""Common setup for all Tailwind commands."""
|
|
1323
|
-
_download_cli()
|
|
1324
|
-
_create_standard_config()
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
1305
|
def _setup_tailwind_environment_with_verbose(*, verbose: bool = False) -> None:
|
|
1328
1306
|
"""Common setup for all Tailwind commands with verbose logging."""
|
|
1329
1307
|
if verbose:
|
|
@@ -1563,11 +1541,6 @@ DEFAULT_SOURCE_CSS = '@import "tailwindcss";\n'
|
|
|
1563
1541
|
DAISY_UI_SOURCE_CSS = '@import "tailwindcss";\n@plugin "daisyui";\n'
|
|
1564
1542
|
|
|
1565
1543
|
|
|
1566
|
-
def _create_standard_config() -> None:
|
|
1567
|
-
"""Create a standard Tailwind CSS config file with optimization."""
|
|
1568
|
-
_create_standard_config_with_verbose(verbose=False)
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
1544
|
def _create_standard_config_with_verbose(*, verbose: bool = False) -> None:
|
|
1572
1545
|
"""Create a standard Tailwind CSS config file with optional verbose logging."""
|
|
1573
1546
|
c = get_config()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utilities for django-tailwind-cli."""
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""HTTP utilities using urllib instead of requests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import socket
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from urllib.error import HTTPError as UrllibHTTPError
|
|
10
|
+
from urllib.error import URLError
|
|
11
|
+
from urllib.request import Request, urlopen
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RequestError(Exception):
|
|
18
|
+
"""Base exception for HTTP requests."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HTTPError(RequestError):
|
|
22
|
+
"""HTTP status error."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NetworkConnectionError(RequestError):
|
|
26
|
+
"""Network connection error."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RequestTimeoutError(RequestError):
|
|
30
|
+
"""Request timeout error."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def fetch_redirect_location(url: str, timeout: int = 10) -> tuple[bool, str | None]:
|
|
34
|
+
"""Fetch redirect location from a URL.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
url: URL to fetch from
|
|
38
|
+
timeout: Request timeout in seconds
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Tuple of (success, location_header)
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
RequestError: On network or HTTP errors
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
req = Request(url)
|
|
48
|
+
# Set User-Agent to avoid blocking
|
|
49
|
+
req.add_header("User-Agent", "django-tailwind-cli")
|
|
50
|
+
|
|
51
|
+
with urlopen(req, timeout=timeout) as response:
|
|
52
|
+
# Check if it's a redirect status
|
|
53
|
+
if response.getcode() in (301, 302, 303, 307, 308):
|
|
54
|
+
location = response.headers.get("Location")
|
|
55
|
+
return True, location
|
|
56
|
+
elif response.getcode() == 200:
|
|
57
|
+
return True, None
|
|
58
|
+
else:
|
|
59
|
+
return False, None
|
|
60
|
+
|
|
61
|
+
except UrllibHTTPError as e:
|
|
62
|
+
# Handle redirect responses that urllib might treat as errors
|
|
63
|
+
if e.code in (301, 302, 303, 307, 308):
|
|
64
|
+
location = e.headers.get("Location")
|
|
65
|
+
return True, location
|
|
66
|
+
return False, None
|
|
67
|
+
except URLError as e:
|
|
68
|
+
if isinstance(e.reason, socket.timeout):
|
|
69
|
+
raise RequestTimeoutError(f"Request timeout: {e}") from e
|
|
70
|
+
elif isinstance(e.reason, (ConnectionRefusedError, socket.gaierror)):
|
|
71
|
+
raise NetworkConnectionError(f"Connection error: {e}") from e
|
|
72
|
+
else:
|
|
73
|
+
raise RequestError(f"URL error: {e}") from e
|
|
74
|
+
except TimeoutError as e:
|
|
75
|
+
raise RequestTimeoutError(f"Socket timeout: {e}") from e
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise RequestError(f"Unexpected error: {e}") from e
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def download_with_progress(
|
|
81
|
+
url: str, filepath: Path, timeout: int = 30, progress_callback: Callable[[int, int, float], None] | None = None
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Download a file with progress indication.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
url: Download URL
|
|
87
|
+
filepath: Destination file path
|
|
88
|
+
timeout: Request timeout in seconds
|
|
89
|
+
progress_callback: Optional callback for progress updates
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
RequestError: On network or HTTP errors
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
req = Request(url)
|
|
96
|
+
req.add_header("User-Agent", "django-tailwind-cli")
|
|
97
|
+
|
|
98
|
+
with urlopen(req, timeout=timeout) as response:
|
|
99
|
+
# Check for HTTP errors
|
|
100
|
+
if response.getcode() >= 400:
|
|
101
|
+
raise HTTPError(f"HTTP {response.getcode()}: {response.reason}")
|
|
102
|
+
|
|
103
|
+
# Get content length for progress tracking
|
|
104
|
+
content_length_header = response.headers.get("Content-Length")
|
|
105
|
+
total_size = int(content_length_header) if content_length_header else 0
|
|
106
|
+
|
|
107
|
+
# Ensure parent directory exists
|
|
108
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
|
|
110
|
+
downloaded = 0
|
|
111
|
+
chunk_size = 8192
|
|
112
|
+
|
|
113
|
+
with filepath.open("wb") as f:
|
|
114
|
+
while True:
|
|
115
|
+
chunk = response.read(chunk_size)
|
|
116
|
+
if not chunk:
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
f.write(chunk)
|
|
120
|
+
downloaded += len(chunk)
|
|
121
|
+
|
|
122
|
+
# Call progress callback if provided
|
|
123
|
+
if progress_callback and total_size > 0:
|
|
124
|
+
progress = (downloaded / total_size) * 100
|
|
125
|
+
progress_callback(downloaded, total_size, progress)
|
|
126
|
+
|
|
127
|
+
except UrllibHTTPError as e:
|
|
128
|
+
raise HTTPError(f"HTTP {e.code}: {e.reason}") from e
|
|
129
|
+
except URLError as e:
|
|
130
|
+
if isinstance(e.reason, socket.timeout):
|
|
131
|
+
raise RequestTimeoutError(f"Download timeout: {e}") from e
|
|
132
|
+
elif isinstance(e.reason, (ConnectionRefusedError, socket.gaierror)):
|
|
133
|
+
raise NetworkConnectionError(f"Connection error: {e}") from e
|
|
134
|
+
else:
|
|
135
|
+
raise RequestError(f"URL error: {e}") from e
|
|
136
|
+
except TimeoutError as e:
|
|
137
|
+
raise RequestTimeoutError(f"Download timeout: {e}") from e
|
|
138
|
+
except OSError as e:
|
|
139
|
+
raise RequestError(f"File error: {e}") from e
|
|
140
|
+
except Exception as e:
|
|
141
|
+
raise RequestError(f"Unexpected error: {e}") from e
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_content_sync(url: str, timeout: int = 30) -> bytes:
|
|
145
|
+
"""Get content from URL synchronously.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
url: URL to fetch from
|
|
149
|
+
timeout: Request timeout in seconds
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Response content as bytes
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
RequestError: On network or HTTP errors
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
req = Request(url)
|
|
159
|
+
req.add_header("User-Agent", "django-tailwind-cli")
|
|
160
|
+
|
|
161
|
+
with urlopen(req, timeout=timeout) as response:
|
|
162
|
+
if response.getcode() >= 400:
|
|
163
|
+
raise HTTPError(f"HTTP {response.getcode()}: {response.reason}")
|
|
164
|
+
return response.read()
|
|
165
|
+
|
|
166
|
+
except UrllibHTTPError as e:
|
|
167
|
+
raise HTTPError(f"HTTP {e.code}: {e.reason}") from e
|
|
168
|
+
except URLError as e:
|
|
169
|
+
if isinstance(e.reason, socket.timeout):
|
|
170
|
+
raise RequestTimeoutError(f"Request timeout: {e}") from e
|
|
171
|
+
elif isinstance(e.reason, (ConnectionRefusedError, socket.gaierror)):
|
|
172
|
+
raise NetworkConnectionError(f"Connection error: {e}") from e
|
|
173
|
+
else:
|
|
174
|
+
raise RequestError(f"URL error: {e}") from e
|
|
175
|
+
except TimeoutError as e:
|
|
176
|
+
raise RequestTimeoutError(f"Request timeout: {e}") from e
|
|
177
|
+
except Exception as e:
|
|
178
|
+
raise RequestError(f"Unexpected error: {e}") from e
|
|
@@ -6,6 +6,7 @@ Also includes tests for error handling and edge cases.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
|
|
10
11
|
import pytest
|
|
11
12
|
from django.conf import LazySettings
|
|
@@ -30,7 +31,14 @@ def configure_test_settings(settings: LazySettings, tmp_path: Path, mocker: Mock
|
|
|
30
31
|
|
|
31
32
|
# Mock subprocess to avoid actual CLI calls
|
|
32
33
|
mocker.patch("subprocess.run")
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
def mock_download(
|
|
36
|
+
url: str, filepath: Path, timeout: int = 30, progress_callback: Callable[[int, int, float], None] | None = None
|
|
37
|
+
) -> None:
|
|
38
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
filepath.write_bytes(b"fake binary content")
|
|
40
|
+
|
|
41
|
+
mocker.patch("django_tailwind_cli.utils.http.download_with_progress", side_effect=mock_download)
|
|
34
42
|
|
|
35
43
|
|
|
36
44
|
class TestConfigCommand:
|