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.
Files changed (57) hide show
  1. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.github/workflows/release.yml +7 -7
  2. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.github/workflows/test.yml +2 -2
  3. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.pre-commit-config.yaml +5 -0
  4. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/CHANGELOG.md +12 -0
  5. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/CLAUDE.md +33 -3
  6. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/PKG-INFO +3 -3
  7. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/README.md +1 -1
  8. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/settings.md +2 -2
  9. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/usage.md +2 -2
  10. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/pyproject.toml +5 -14
  11. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/config.py +11 -8
  12. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/management/commands/tailwind.py +17 -44
  13. django_tailwind_cli-4.4.0/src/django_tailwind_cli/utils/__init__.py +1 -0
  14. django_tailwind_cli-4.4.0/src/django_tailwind_cli/utils/http.py +178 -0
  15. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_additional_commands.py +9 -1
  16. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_config.py +11 -12
  17. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_error_scenarios.py +85 -82
  18. django_tailwind_cli-4.4.0/tests/test_http.py +148 -0
  19. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_integration.py +191 -96
  20. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_management_commands.py +46 -5
  21. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tox.ini +11 -6
  22. django_tailwind_cli-4.4.0/uv.lock +507 -0
  23. django_tailwind_cli-4.3.0/uv.lock +0 -693
  24. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.github/dependabot.yml +0 -0
  25. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.gitignore +0 -0
  26. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/.readthedocs.yml +0 -0
  27. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/CODE_OF_CONDUCT.md +0 -0
  28. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/CONFIGURATION.md +0 -0
  29. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/DEVELOPMENT.md +0 -0
  30. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/LICENSE +0 -0
  31. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/base_template.md +0 -0
  32. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/changelog.md +0 -0
  33. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/index.md +0 -0
  34. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/installation.md +0 -0
  35. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/requirements.txt +0 -0
  36. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/docs/template_tags.md +0 -0
  37. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/justfile +0 -0
  38. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/mkdocs.yml +0 -0
  39. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/__init__.py +0 -0
  40. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/apps.py +0 -0
  41. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/management/__init__.py +0 -0
  42. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/management/commands/__init__.py +0 -0
  43. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/py.typed +0 -0
  44. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/templates/tailwind_cli/base.html +0 -0
  45. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/templates/tailwind_cli/tailwind_css.html +0 -0
  46. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/templatetags/__init__.py +0 -0
  47. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/src/django_tailwind_cli/templatetags/tailwind_cli.py +0 -0
  48. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/.gitignore +0 -0
  49. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/__init__.py +0 -0
  50. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/assets/css/.gitkeep +0 -0
  51. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/assets/css/tailwind.css +0 -0
  52. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/conftest.py +0 -0
  53. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/settings.py +0 -0
  54. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/templates/tests/base.html +0 -0
  55. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/templates/tests/dummy.email +0 -0
  56. {django_tailwind_cli-4.3.0 → django_tailwind_cli-4.4.0}/tests/test_get_runserver_options.py +0 -0
  57. {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@v4
17
+ - uses: actions/checkout@v5
18
18
  - name: Set up Python
19
- uses: actions/setup-python@v5
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@v4
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.12
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@v4
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@v4
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.12
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@v4
17
+ - uses: actions/checkout@v5
18
18
 
19
19
  - name: Set up Python ${{ matrix.python-version }}
20
- uses: actions/setup-python@v5
20
+ uses: actions/setup-python@v6
21
21
  with:
22
22
  python-version: ${{ matrix.python-version }}
23
23
  allow-prereleases: true
@@ -43,3 +43,8 @@ repos:
43
43
  rev: 0.11.1
44
44
  hooks:
45
45
  - id: uv-secure
46
+
47
+ - repo: https://github.com/RobertCraigie/pyright-python
48
+ rev: v1.1.403
49
+ hooks:
50
+ - id: pyright
@@ -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-5.2
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.0
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**: `.django_tailwind_cli/source.css`
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
- # mypy
71
- [tool.mypy]
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 requests
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 _get_platform_info() -> PlatformInfo:
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
- r = requests.get(f"https://github.com/{repo_url}/releases/latest/", timeout=timeout, allow_redirects=False)
292
- if r.ok and "location" in r.headers:
293
- version_str = r.headers["location"].rstrip("/").split("/")[-1].replace("v", "")
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 (requests.RequestException, ValueError):
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 = _get_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 requests
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 _get_platform_info
583
+ from django_tailwind_cli.config import get_platform_info
583
584
 
584
- platform_info = _get_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
- try:
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
- # Show progress every 10%
1310
- if total_size > 0:
1311
- progress = (downloaded / total_size) * 100
1312
- if downloaded % (total_size // 10 + 1) < 8192:
1313
- typer.secho(f"Progress: {progress:.1f}%", fg=typer.colors.CYAN)
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 requests.RequestException as e:
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
- mocker.patch("requests.get").return_value.content = b"fake binary content"
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: