Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b1f562649 | ||
|
|
0ce6342ca7 | ||
|
|
0c7b007729 | ||
|
|
eaddc4576d | ||
|
|
d741249644 | ||
|
|
dee852efb9 | ||
|
|
fc93a54430 | ||
|
|
04c911c3d9 | ||
|
|
8bb81a1a38 | ||
|
|
5013c1a31a | ||
|
|
d79698cfb4 | ||
|
|
e5f77143bf | ||
|
|
7d91c037ca | ||
|
|
ee0b22f2ae | ||
|
|
4c40dd21fd | ||
|
|
09fa597215 | ||
|
|
123141bd32 | ||
|
|
72fc023fd3 | ||
|
|
c4f66d607b | ||
|
|
0a4aa2c39a | ||
|
|
aba0a8f6f7 | ||
|
|
415a7c2ea5 | ||
|
|
2895635c99 | ||
|
|
55a20639de | ||
|
|
6668365df4 | ||
|
|
65f300e774 | ||
|
|
35155d0898 | ||
|
|
56b2b8574d | ||
|
|
d68bc0a98c | ||
|
|
0ba3d0c8d1 | ||
|
|
1e14465cbb | ||
|
|
a3d3e5bdd5 | ||
|
|
4f3cb3c3c9 | ||
|
|
afac4dc36a | ||
|
|
4ddcb1d052 | ||
|
|
b131f48619 | ||
|
|
a5ea1800d9 | ||
|
|
4a03328b33 | ||
|
|
cb139a1efc | ||
|
|
a0e4fc2f07 | ||
|
|
9570aa7220 | ||
|
|
1be862368d | ||
|
|
6229e17f42 | ||
|
|
6c56689d8c | ||
|
|
48177d969b | ||
|
|
992688641e | ||
|
|
c42e0635b8 | ||
|
|
68f1b44eab | ||
|
|
4ecea0afe3 | ||
|
|
aefeeadf3e | ||
|
|
a76e7e388b | ||
|
|
afb01bfefb | ||
|
|
254dd6d5c8 | ||
|
|
e24fb36dea | ||
|
|
7d76fb902c | ||
|
|
866ddd0105 | ||
|
|
7d83f41c48 | ||
|
|
a3d20860b6 | ||
|
|
a293ad9771 | ||
|
|
0cff748d5e | ||
|
|
85981115ce | ||
|
|
744f65efc7 | ||
|
|
995d0624e4 | ||
|
|
e5f05681d1 | ||
|
|
64edec1cc2 | ||
|
|
f82bf1172d | ||
|
|
cab69c2232 | ||
|
|
41782c3df0 | ||
|
|
0fde5c7aa8 | ||
|
|
bc23b02007 | ||
|
|
a65e12442e | ||
|
|
6cc5fe43e6 | ||
|
|
9d699abea3 | ||
|
|
3862722e2b | ||
|
|
a7b92c72cd | ||
|
|
e5a49f1a89 | ||
|
|
0cffc3926d | ||
|
|
eedb3e3677 | ||
|
|
37e2ecff49 | ||
|
|
370215fcc2 | ||
|
|
77416afde8 | ||
|
|
22839528d2 | ||
|
|
93094cb7fe | ||
|
|
492f478e92 | ||
|
|
1ac64d385c | ||
|
|
665cd62f5a | ||
|
|
39b05e41f8 | ||
|
|
3da282d80d | ||
|
|
a3e50626a0 | ||
|
|
b176374f8c | ||
|
|
2df739cff6 | ||
|
|
c0dac9b67d | ||
|
|
38bcfe59b5 | ||
|
|
6edff2b65a |
No files matched your search
@@ -31,15 +31,15 @@ jobs:
|
||||
|
||||
- name: Lint with Ruff
|
||||
run: |
|
||||
ruff check djinit/
|
||||
ruff check src/djinit/
|
||||
|
||||
- name: Check formatting with Ruff
|
||||
run: |
|
||||
ruff format --check djinit/
|
||||
ruff format --check src/djinit/
|
||||
|
||||
- name: Test CLI import
|
||||
run: |
|
||||
python -c "from djinit.generator import main; print('Import successful')"
|
||||
python -c "from djinit.cli import main; print('Import successful')"
|
||||
|
||||
- name: Test console script entry point
|
||||
run: |
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
name: Dependabot Auto-Merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -10,11 +10,13 @@ permissions:
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
if: ${{github.actor == 'dependabot[bot]' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Check that it's actually dependabot
|
||||
if: github.event.pull_request.user.login == 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Merge PR from dependabot
|
||||
- name: Merge PR from Dependabot
|
||||
uses: fastify/github-action-merge-dependabot@v3
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
use-github-auto-merge: true
|
||||
target: minor
|
||||
@@ -1,6 +1,7 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
AGENTS.md
|
||||
|
||||
# Python
|
||||
*.py[cod]
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
**djinit** helps you set up a production-ready Django project in minutes. No more copy-pasting settings or manually wiring up apps, just answer a few questions and get a modern Django project with REST API, authentication, documentation, and deployment configs ready to go.
|
||||
**djinit** helps you set up a production‑ready Django project in minutes. No more copy‑pasting settings or manually wiring up apps—just answer a few questions and get a modern Django project with REST API, authentication, documentation, and deployment configs ready to go.
|
||||
|
||||
## Why djinit?
|
||||
|
||||
@@ -37,63 +37,51 @@ Or with uv:
|
||||
uv tool install djinitx
|
||||
```
|
||||
|
||||
**Requirements**: Python 3.13+
|
||||
**Requirements**: Python 3.13+
|
||||
|
||||
## Getting Started
|
||||
|
||||
Just run:
|
||||
|
||||
```bash
|
||||
djinit setup
|
||||
```
|
||||
|
||||
You can also use the shorter alias:
|
||||
or the shorter alias:
|
||||
|
||||
```bash
|
||||
dj setup
|
||||
```
|
||||
|
||||
The tool will ask you a few questions:
|
||||
The wizard will ask you a few questions:
|
||||
|
||||
1. **What structure do you want?**
|
||||
- **Standard**: Classic Django layout with split settings
|
||||
- **Predefined**: Organized with `apps/` and `api/` folders (great for larger projects)
|
||||
- **Unified**: Everything under `core/` and `apps/` (clean and minimal)
|
||||
- **Single Folder**: All apps in one configurable folder (simple and flat)
|
||||
- **Standard** – Classic Django layout with split settings
|
||||
- **Predefined** – Organized with `apps/` and `api/` folders (great for larger projects)
|
||||
- **Unified** – Everything under `core/` and `apps/` (clean and minimal)
|
||||
- **Single Folder** – All apps in one configurable folder (simple and flat)
|
||||
|
||||
2. **Project Setup**:
|
||||
- Where to create it (use `.` for current directory)
|
||||
- Project name (or directory name for Single Folder)
|
||||
2. **Project Setup** – Destination directory (use `.` for current) and project name.
|
||||
|
||||
3. **Database Configuration**:
|
||||
- Use `DATABASE_URL` (cleaner, recommended)
|
||||
- Or separate DB variables
|
||||
- Choose between PostgreSQL or MySQL
|
||||
3. **Database Configuration** – Choose `DATABASE_URL` (recommended) or individual variables; pick PostgreSQL or MySQL.
|
||||
|
||||
4. **Django Apps** (Standard Structure only):
|
||||
- Whether to use an `apps/` folder
|
||||
- Which apps to create
|
||||
4. **Django Apps** *(Standard structure only)* – Whether to create an `apps/` folder and which apps to scaffold.
|
||||
|
||||
5. **CI/CD Pipeline**:
|
||||
- GitHub Actions, GitLab CI, both, or skip it
|
||||
5. **CI/CD Pipeline** – GitHub Actions, GitLab CI, both, or skip it.
|
||||
|
||||
That's it! Your project will be ready with everything configured.
|
||||
That’s it—your project will be ready with everything configured.
|
||||
|
||||
## What You Get
|
||||
|
||||
Every project includes:
|
||||
|
||||
- **Split settings** for development and production
|
||||
- **Django REST Framework** with JWT authentication
|
||||
- **API documentation** (Swagger UI at `/docs/`)
|
||||
- **CORS** configured for local development
|
||||
- **WhiteNoise** for serving static files
|
||||
- **WhiteNoise** for static files
|
||||
- **PostgreSQL** support (SQLite for dev)
|
||||
- **Modern admin** interface (django-jazzmin)
|
||||
- **Deployment ready** with Procfile and runtime.txt
|
||||
- **Development tools** (Justfile with common commands)
|
||||
- **Environment template** (.env.sample)
|
||||
- **Git ready** (.gitignore included)
|
||||
- **Modern admin** interface (`django-jazzmin`)
|
||||
- **Deployment ready** with `Procfile` and `runtime.txt`
|
||||
- **Development tools** (`Justfile` with common commands)
|
||||
- **Environment template** (`.env.sample`)
|
||||
- **Git ready** (`.gitignore` included)
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -103,13 +91,13 @@ Every project includes:
|
||||
djinit setup
|
||||
```
|
||||
|
||||
### Add Apps to Existing Project
|
||||
### Add Apps to an Existing Project
|
||||
|
||||
```bash
|
||||
djinit app users products orders
|
||||
```
|
||||
|
||||
This automatically creates the apps, adds them to `INSTALLED_APPS`, and wires up URLs.
|
||||
Creates the apps, adds them to `INSTALLED_APPS`, and wires up URLs.
|
||||
|
||||
### Generate Secret Keys
|
||||
|
||||
@@ -117,14 +105,12 @@ This automatically creates the apps, adds them to `INSTALLED_APPS`, and wires up
|
||||
djinit secret
|
||||
```
|
||||
|
||||
Need more? Use `--count 5` or change length with `--length 64`.
|
||||
Use `--count 5` to generate five keys or `--length 64` to change the length.
|
||||
|
||||
## Project Structures
|
||||
|
||||
### Standard Structure
|
||||
|
||||
The classic Django layout with split settings:
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
@@ -138,8 +124,6 @@ myproject/
|
||||
|
||||
### Single Folder Layout
|
||||
|
||||
Simple structure with all apps in one folder:
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
@@ -147,7 +131,7 @@ myproject/
|
||||
│ ├── settings/
|
||||
│ ├── urls.py
|
||||
│ ├── models/ # All models here
|
||||
│ ├── api/ # API views and serializers
|
||||
│ ├── api/ # API views & serializers
|
||||
│ │ └── your_model_name/
|
||||
│ │ ├── views.py
|
||||
│ │ ├── serializers.py
|
||||
@@ -157,8 +141,6 @@ myproject/
|
||||
|
||||
### Predefined Structure
|
||||
|
||||
Organized for larger projects:
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
@@ -174,8 +156,6 @@ myproject/
|
||||
|
||||
### Unified Structure
|
||||
|
||||
Clean and minimal:
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
@@ -193,7 +173,7 @@ myproject/
|
||||
|
||||
## Development Workflow
|
||||
|
||||
Your project comes with a `Justfile` for common tasks:
|
||||
Your project ships with a `Justfile` for common tasks:
|
||||
|
||||
```bash
|
||||
just dev # Start dev server
|
||||
@@ -205,52 +185,90 @@ just format # Format code
|
||||
just lint # Lint code
|
||||
```
|
||||
|
||||
Don't have `just` installed? No problem—these are just shortcuts for standard Django commands.
|
||||
If you don’t have `just` installed, these are just shortcuts for the equivalent Django commands.
|
||||
|
||||
## What's Included
|
||||
|
||||
### Packages
|
||||
|
||||
- Django (web framework)
|
||||
- Django REST Framework (API)
|
||||
- djangorestframework-simplejwt (JWT auth)
|
||||
- drf-spectacular (API docs)
|
||||
- django-cors-headers (CORS)
|
||||
- django-jazzmin (modern admin)
|
||||
- whitenoise (static files)
|
||||
- psycopg2-binary (PostgreSQL)
|
||||
- gunicorn (production server)
|
||||
- python-dotenv (environment variables)
|
||||
- **Django** – Web framework
|
||||
- **Django REST Framework** – API toolkit
|
||||
- **djangorestframework‑simplejwt** – JWT authentication
|
||||
- **drf‑spectacular** – OpenAPI/Swagger docs
|
||||
- **django‑cors‑headers** – CORS handling
|
||||
- **django‑jazzmin** – Modern admin UI
|
||||
- **whitenoise** – Static file serving
|
||||
- **psycopg2‑binary** – PostgreSQL driver
|
||||
- **gunicorn** – Production WSGI server
|
||||
- **python‑dotenv** – `.env` handling
|
||||
|
||||
### API Endpoints
|
||||
|
||||
- `/admin/` - Django admin
|
||||
- `/token/` - Get JWT token
|
||||
- `/token/refresh/` - Refresh token
|
||||
- `/docs/` - Swagger UI (dev only)
|
||||
- `/schema/` - OpenAPI schema (dev only)
|
||||
| Endpoint | Description |
|
||||
|-------------------|---------------------------------|
|
||||
| `/admin/` | Django admin |
|
||||
| `/token/` | Obtain JWT token |
|
||||
| `/token/refresh/` | Refresh JWT token |
|
||||
| `/docs/` | Swagger UI (dev only) |
|
||||
| `/schema/` | OpenAPI schema (dev only) |
|
||||
|
||||
### Settings
|
||||
### Settings Overview
|
||||
|
||||
**Development**: SQLite, debug mode, console emails, permissive CORS
|
||||
**Production**: PostgreSQL, security hardened, SMTP emails, strict CORS
|
||||
- **Development** – SQLite, `DEBUG=True`, console email backend, permissive CORS.
|
||||
- **Production** – PostgreSQL, hardened security settings, SMTP email backend, strict CORS.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Copy `.env.sample` to `.env` and fill in your values:
|
||||
Copy the sample file and fill in your values:
|
||||
|
||||
```bash
|
||||
SECRET_KEY=your-secret-key-here # Use: djinit secret
|
||||
cp .env.sample .env
|
||||
```
|
||||
|
||||
```dotenv
|
||||
SECRET_KEY=your-secret-key-here # Generate with: djinit secret
|
||||
DATABASE_URL=postgres://user:pass@host:5432/db
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||
```
|
||||
|
||||
For development, SQLite works out of the box—no database setup needed!
|
||||
SQLite works out of the box for development—no extra DB setup required.
|
||||
|
||||
## Roadmap
|
||||
|
||||
The following items are tracked in **TODO.md** and represent the near‑future direction of djinit.
|
||||
|
||||
### Planned Features
|
||||
- **Docker Support** – Auto‑generate a `Dockerfile` for containerized deployments.
|
||||
- **Frontend Integration** – Scaffold React, Vue, or HTMX alongside the Django backend.
|
||||
- **Celery Integration** – Simplify background task setup with Celery.
|
||||
- **More Packages** – Add optional integrations for popular Django packages.
|
||||
|
||||
### Enhancements
|
||||
- **Add more project structure templates** – Expand the set of ready‑made layouts.
|
||||
- **Add testing framework setup** – Provide pytest and coverage configuration out of the box.
|
||||
- **Fix bugs** – Ongoing maintenance and bug resolution.
|
||||
|
||||
### Completed
|
||||
- **Interactive configuration wizard** – Streamlined project creation experience.
|
||||
- **Improved structure detection** – Smarter detection of existing Django layouts.
|
||||
|
||||
> Contributions that address any of the above items are highly welcome!
|
||||
|
||||
## Contributing
|
||||
|
||||
Found a bug or have an idea? Open an issue or submit a pull request. Contributions are always welcome!
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a feature branch (`git checkout -b feature/awesome-feature`).
|
||||
3. Make your changes and ensure tests pass (`just test`).
|
||||
4. Submit a pull request with a clear description of the change.
|
||||
|
||||
Please follow the existing code style (ruff + black) and include tests for new functionality.
|
||||
|
||||
## License
|
||||
|
||||
MIT © Sankalp Tharu
|
||||
|
||||
---
|
||||
|
||||
*© 2024 Sankalp Tharu. All rights reserved.*
|
||||
@@ -13,3 +13,4 @@
|
||||
- [x] Add interactive configuration wizard
|
||||
- [ ] Add testing framework setup (pytest, coverage)
|
||||
- [x] Improve structure detection
|
||||
- [ ] Fix bugs
|
||||
@@ -1,110 +0,0 @@
|
||||
"""
|
||||
Command handlers for Django project setup CLI.
|
||||
Handles different commands like secret key generation and app creation.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from djinit.services.app import AppManager
|
||||
from djinit.ui.console import UIColors, UIFormatter, console
|
||||
from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys
|
||||
from djinit.utils.validators import validate_app_name
|
||||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Django project setup tool", prog="django-init")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
setup_parser = subparsers.add_parser("setup", help="Create a new Django project")
|
||||
setup_parser.add_argument("--project", help="Django project name")
|
||||
setup_parser.add_argument("--app", help="Django app name")
|
||||
|
||||
secret_parser = subparsers.add_parser("secret", help="Generate Django secret keys")
|
||||
secret_parser.add_argument("--count", type=int, default=3, help="Number of keys to generate")
|
||||
secret_parser.add_argument("--length", type=int, default=50, help="Length of each secret key")
|
||||
|
||||
app_parser = subparsers.add_parser("app", help="Create one or more Django apps (comma or space separated)")
|
||||
app_parser.add_argument(
|
||||
"app_name",
|
||||
nargs="+",
|
||||
help=(
|
||||
"App name(s) or comma-separated list. Examples: \n"
|
||||
" djinit app users \n"
|
||||
" djinit app users,products,orders \n"
|
||||
" djinit app users products orders \n"
|
||||
" djinit app users, products, orders"
|
||||
),
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def handle_secret_command(args: argparse.Namespace) -> None:
|
||||
keys = generate_multiple_keys(args.count, args.length)
|
||||
UIFormatter.print_info("")
|
||||
display_secret_keys(keys)
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT)
|
||||
instructions.append("1. Copy the appropriate secret key for your environment\n")
|
||||
instructions.append("2. Add it to your .env file:\n")
|
||||
instructions.append(" SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
|
||||
instructions.append("3. Or set it as an environment variable:\n")
|
||||
instructions.append(" export SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
|
||||
instructions.append("4. Never commit secret keys to version control!\n", style=UIColors.WARNING)
|
||||
|
||||
console.print(Panel(instructions, title="💡 How to Use", border_style="blue"))
|
||||
console.print()
|
||||
|
||||
|
||||
def handle_app_command(args: argparse.Namespace) -> None:
|
||||
UIFormatter.print_info("")
|
||||
UIFormatter.print_header("Django App Creation")
|
||||
UIFormatter.print_info("")
|
||||
|
||||
raw_tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
|
||||
|
||||
# Flatten and clean tokens
|
||||
app_names = [part.strip() for token in raw_tokens if token for part in str(token).split(",") if part.strip()]
|
||||
|
||||
if not app_names:
|
||||
UIFormatter.print_error("App name cannot be empty")
|
||||
sys.exit(1)
|
||||
|
||||
# Deduplicate while preserving order
|
||||
deduped_names = list(dict.fromkeys(app_names))
|
||||
|
||||
for name in deduped_names:
|
||||
is_valid, error_msg = validate_app_name(name)
|
||||
if not is_valid:
|
||||
UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}")
|
||||
sys.exit(1)
|
||||
|
||||
any_failure = False
|
||||
for name in deduped_names:
|
||||
app_manager = AppManager(name)
|
||||
success = app_manager.create_app()
|
||||
if not success:
|
||||
UIFormatter.print_error(f"Failed to create Django app '{name}'")
|
||||
any_failure = True
|
||||
break
|
||||
UIFormatter.print_success(f"Django app '{name}' created successfully!")
|
||||
|
||||
if any_failure:
|
||||
sys.exit(1)
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT)
|
||||
instructions.append("1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n")
|
||||
instructions.append("2. Create your models in each app's models.py\n")
|
||||
instructions.append("3. Run migrations: just makemigrations\n")
|
||||
instructions.append("4. Apply migrations: just migrate\n")
|
||||
instructions.append("5. Create views, serializers and routes(URLs) for your app(s)\n")
|
||||
|
||||
console.print(Panel(instructions, title="💡 What's Next", border_style="green"))
|
||||
UIFormatter.print_info("")
|
||||
@@ -1,61 +0,0 @@
|
||||
"""
|
||||
Main script entry point for djinit.
|
||||
Handles user input validation and orchestrates the setup process.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from djinit.cli import Cli
|
||||
from djinit.commands.handler import handle_app_command, handle_secret_command, parse_arguments
|
||||
from djinit.ui.console import UIFormatter, console
|
||||
from djinit.ui.input import confirm_setup, get_user_input
|
||||
|
||||
|
||||
def clear_screen() -> None:
|
||||
try:
|
||||
if os.name == "nt":
|
||||
subprocess.run("cls", shell=True, check=True)
|
||||
else:
|
||||
subprocess.run("clear", shell=True, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
console.print("\n" * 50)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_arguments()
|
||||
|
||||
if args.command == "secret":
|
||||
handle_secret_command(args)
|
||||
return
|
||||
|
||||
if args.command == "app":
|
||||
handle_app_command(args)
|
||||
return
|
||||
|
||||
clear_screen()
|
||||
|
||||
console.print()
|
||||
console.print(UIFormatter.create_welcome_panel())
|
||||
console.print()
|
||||
|
||||
project_dir, project_name, primary_app, app_names, metadata = get_user_input()
|
||||
|
||||
if not confirm_setup(project_dir, project_name, app_names, metadata):
|
||||
UIFormatter.print_info("Setup cancelled by user.")
|
||||
return
|
||||
|
||||
console.print()
|
||||
UIFormatter.print_info("Starting Django project setup...")
|
||||
console.print()
|
||||
|
||||
django_cli = Cli(project_dir, project_name, primary_app, app_names, metadata)
|
||||
success = django_cli.run_setup()
|
||||
|
||||
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True, metadata=metadata)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,264 +0,0 @@
|
||||
"""
|
||||
App management for djinit.
|
||||
Handles creation of Django apps and updating settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from djinit.ui.console import UIFormatter
|
||||
from djinit.utils.common import (
|
||||
calculate_app_module_path,
|
||||
create_directory_with_init,
|
||||
create_file_from_template,
|
||||
create_init_file,
|
||||
detect_nested_structure_from_settings,
|
||||
extract_existing_apps,
|
||||
find_project_dir,
|
||||
find_settings_path,
|
||||
get_djinit_config,
|
||||
insert_apps_into_user_defined_apps,
|
||||
is_django_project,
|
||||
)
|
||||
from djinit.utils.django import DjangoHelper
|
||||
|
||||
|
||||
class AppManager:
|
||||
def __init__(self, app_name: str):
|
||||
self.app_name = app_name
|
||||
self.current_dir = os.getcwd()
|
||||
self.manage_py_path = os.path.join(self.current_dir, "manage.py")
|
||||
self.config = get_djinit_config(self.current_dir)
|
||||
self._project_structure_cache = None
|
||||
|
||||
def create_app(self) -> bool:
|
||||
if not self._is_django_project():
|
||||
UIFormatter.print_error(
|
||||
"Not in a Django project directory. Please run this command from your Django project root."
|
||||
)
|
||||
return False
|
||||
|
||||
if self._is_restricted_structure():
|
||||
UIFormatter.print_error("The 'app' command is not supported for Unified or Single Folder structures.")
|
||||
UIFormatter.print_info(
|
||||
"Please add new apps manually by creating the necessary files and updating settings."
|
||||
)
|
||||
return False
|
||||
|
||||
if self._app_exists():
|
||||
UIFormatter.print_error(f"Django app '{self.app_name}' already exists.")
|
||||
return False
|
||||
|
||||
if not self._create_django_app():
|
||||
return False
|
||||
|
||||
if not self._add_to_installed_apps():
|
||||
return False
|
||||
|
||||
UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!")
|
||||
return True
|
||||
|
||||
def _is_django_project(self) -> bool:
|
||||
return is_django_project(self.current_dir)
|
||||
|
||||
def _app_exists(self) -> bool:
|
||||
# For predefined structure, check in apps/ directory
|
||||
if self._is_predefined_structure():
|
||||
app_path = os.path.join(self.current_dir, "apps", self.app_name)
|
||||
else:
|
||||
_, _, apps_base_dir = self._get_project_structure()
|
||||
app_path = os.path.join(apps_base_dir, self.app_name)
|
||||
return os.path.exists(app_path)
|
||||
|
||||
def _create_django_app(self) -> bool:
|
||||
if not os.path.exists(self.manage_py_path):
|
||||
UIFormatter.print_error("Could not find manage.py file in project root")
|
||||
return False
|
||||
|
||||
if self._is_predefined_structure():
|
||||
return self._create_predefined_app(os.path.join(self.current_dir, "apps"))
|
||||
|
||||
_, _, apps_base_dir = self._get_project_structure()
|
||||
success = DjangoHelper.startapp(self.app_name, apps_base_dir)
|
||||
if success:
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
|
||||
else:
|
||||
UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'")
|
||||
return success
|
||||
|
||||
def _add_to_installed_apps(self) -> bool:
|
||||
settings_path = find_settings_path(self.current_dir)
|
||||
if not settings_path:
|
||||
UIFormatter.print_error("Could not find Django settings directory")
|
||||
return False
|
||||
|
||||
base_settings_path = os.path.join(settings_path, "base.py")
|
||||
if not os.path.exists(base_settings_path):
|
||||
UIFormatter.print_error("Could not find base.py settings file")
|
||||
return False
|
||||
|
||||
with open(base_settings_path) as f:
|
||||
content = f.read()
|
||||
|
||||
if self._is_predefined_structure():
|
||||
app_module_path = f"apps.{self.app_name}"
|
||||
else:
|
||||
is_nested, nested_dir, _ = self._get_project_structure()
|
||||
app_module_path = calculate_app_module_path(self.app_name, is_nested, nested_dir)
|
||||
|
||||
existing_apps = extract_existing_apps(content)
|
||||
if app_module_path in existing_apps:
|
||||
UIFormatter.print_success(f"App '{app_module_path}' already configured in USER_DEFINED_APPS")
|
||||
return True
|
||||
|
||||
if "USER_DEFINED_APPS" not in content:
|
||||
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
|
||||
return False
|
||||
|
||||
updated_content = insert_apps_into_user_defined_apps(content, [app_module_path])
|
||||
if not updated_content:
|
||||
return False
|
||||
|
||||
with open(base_settings_path, "w") as f:
|
||||
f.write(updated_content)
|
||||
|
||||
UIFormatter.print_success(f"Added '{app_module_path}' to USER_DEFINED_APPS in base.py")
|
||||
return True
|
||||
|
||||
def _get_project_structure(self) -> tuple[bool, Optional[str], str]:
|
||||
"""Get project structure with caching to avoid repeated detection."""
|
||||
if self._project_structure_cache is None:
|
||||
if self.config:
|
||||
apps_config = self.config.get("apps", {})
|
||||
nested = apps_config.get("nested", False)
|
||||
nested_dir = apps_config.get("nested_dir")
|
||||
|
||||
# Determine base dir for apps
|
||||
if nested and nested_dir:
|
||||
apps_base_dir = os.path.join(self.current_dir, nested_dir)
|
||||
else:
|
||||
apps_base_dir = self.current_dir
|
||||
|
||||
self._project_structure_cache = (nested, nested_dir, apps_base_dir)
|
||||
else:
|
||||
# Fallback to old detection method
|
||||
project_dir, settings_base_path = find_project_dir(self.current_dir)
|
||||
|
||||
if project_dir is None:
|
||||
self._project_structure_cache = (False, None, self.current_dir)
|
||||
else:
|
||||
self._project_structure_cache = detect_nested_structure_from_settings(
|
||||
settings_base_path, self.current_dir
|
||||
)
|
||||
return self._project_structure_cache
|
||||
|
||||
def _is_predefined_structure(self) -> bool:
|
||||
if self.config:
|
||||
return self.config.get("structure", {}).get("predefined", False)
|
||||
|
||||
apps_dir = os.path.join(self.current_dir, "apps")
|
||||
api_dir = os.path.join(self.current_dir, "api")
|
||||
return os.path.isdir(apps_dir) and os.path.isdir(api_dir)
|
||||
|
||||
def _is_restricted_structure(self) -> bool:
|
||||
"""Check if the project structure is Unified or Single Folder."""
|
||||
if self.config:
|
||||
structure = self.config.get("structure", {})
|
||||
return structure.get("unified", False) or structure.get("single", False)
|
||||
|
||||
# Check for Unified structure (apps/api exists)
|
||||
if os.path.isdir(os.path.join(self.current_dir, "apps", "api")):
|
||||
return True
|
||||
|
||||
# Check for Predefined structure (apps/ exists but not apps/api)
|
||||
if os.path.isdir(os.path.join(self.current_dir, "apps")):
|
||||
return False
|
||||
|
||||
# Check for Single Folder structure (models/ and api/ exist in project dir)
|
||||
project_dir, _ = find_project_dir(self.current_dir)
|
||||
if project_dir:
|
||||
has_models = os.path.isdir(os.path.join(project_dir, "models"))
|
||||
has_api = os.path.isdir(os.path.join(project_dir, "api"))
|
||||
if has_models and has_api:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _create_predefined_app(self, apps_dir: str) -> bool:
|
||||
"""Create an app following the predefined nested structure with custom templates."""
|
||||
app_dir = os.path.join(apps_dir, self.app_name)
|
||||
os.makedirs(app_dir, exist_ok=True)
|
||||
create_init_file(app_dir, f"Created apps/{self.app_name}/__init__.py")
|
||||
|
||||
model_class_name = "".join([part.capitalize() for part in self.app_name.split("_")])
|
||||
app_module = f"apps.{self.app_name}"
|
||||
base_context = {"app_name": self.app_name, "app_module": app_module, "model_class_name": model_class_name}
|
||||
|
||||
create_file_from_template(
|
||||
os.path.join(app_dir, "apps.py"),
|
||||
"components/apps.j2",
|
||||
{"app_name": self.app_name},
|
||||
f"Created apps/{self.app_name}/apps.py",
|
||||
)
|
||||
|
||||
subfolders = {
|
||||
"models": [(f"{self.app_name}.py", "presets/predefined/apps/generic/models.j2")],
|
||||
"serializers": [(f"{self.app_name}_serializer.py", "presets/predefined/apps/generic/serializers.j2")],
|
||||
"services": [(f"{self.app_name}_service.py", "presets/predefined/apps/generic/services.j2")],
|
||||
"views": [(f"{self.app_name}_view.py", "presets/predefined/apps/generic/views.j2")],
|
||||
"tests": [(f"test_{self.app_name}_api.py", "presets/predefined/apps/generic/tests.j2")],
|
||||
}
|
||||
for folder, files in subfolders.items():
|
||||
folder_path = os.path.join(app_dir, folder)
|
||||
create_directory_with_init(folder_path, f"Created apps/{self.app_name}/{folder}/__init__.py")
|
||||
for filename, template in files:
|
||||
file_path = os.path.join(folder_path, filename)
|
||||
create_file_from_template(
|
||||
file_path, template, base_context, f"Created apps/{self.app_name}/{folder}/{filename}"
|
||||
)
|
||||
|
||||
create_file_from_template(
|
||||
os.path.join(app_dir, "urls.py"),
|
||||
"presets/predefined/apps/generic/urls.j2",
|
||||
{"app_name": self.app_name, "app_module": app_module},
|
||||
f"Created apps/{self.app_name}/urls.py",
|
||||
)
|
||||
|
||||
self._add_to_api_v1_urls(self.app_name)
|
||||
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' with predefined structure in {apps_dir}")
|
||||
return True
|
||||
|
||||
def _add_to_api_v1_urls(self, app_name: str) -> None:
|
||||
api_v1_urls = os.path.join(self.current_dir, "api", "v1", "urls.py")
|
||||
if not os.path.exists(api_v1_urls):
|
||||
return
|
||||
try:
|
||||
with open(api_v1_urls, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
import_line = "from django.urls import include, path"
|
||||
if import_line not in content:
|
||||
# ensure base import exists (fallback)
|
||||
content = import_line + "\n\n" + content
|
||||
|
||||
include_stmt = f'path("{app_name}/", include("apps.{app_name}.urls")),'
|
||||
if include_stmt in content:
|
||||
return
|
||||
|
||||
if "urlpatterns = [" in content:
|
||||
parts = content.split("urlpatterns = [", 1)
|
||||
before = parts[0]
|
||||
rest = parts[1]
|
||||
if "]" in rest:
|
||||
idx = rest.rfind("]")
|
||||
new_rest = rest[:idx]
|
||||
if not new_rest.endswith("\n"):
|
||||
new_rest += "\n"
|
||||
new_rest += f" {include_stmt}\n" + rest[idx:]
|
||||
content = before + "urlpatterns = [" + new_rest
|
||||
|
||||
with open(api_v1_urls, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
except Exception:
|
||||
return
|
||||
@@ -1,8 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class {{ app_name|title }}Config(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = '{{ app_name }}'
|
||||
Whitespace-only changes.
@@ -1,18 +0,0 @@
|
||||
"""
|
||||
generated with djinit
|
||||
|
||||
API routes for {{ app_name }} app.
|
||||
"""
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from . import views
|
||||
|
||||
# Create router instance
|
||||
router = DefaultRouter()
|
||||
|
||||
# Register viewsets with router
|
||||
# Example:
|
||||
# router.register(r'{{ app_name }}', views.{{ app_name|title }}ViewSet)
|
||||
|
||||
# Get URL patterns
|
||||
urlpatterns = router.urls
|
||||
@@ -1,14 +0,0 @@
|
||||
"""
|
||||
generated with djinit
|
||||
serializers for {{ app_name }} app.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
# Add your serializers here
|
||||
# Example:
|
||||
# class {{ app_name|title }}Serializer(serializers.ModelSerializer):
|
||||
# class Meta:
|
||||
# model = YourModel
|
||||
# fields = '__all__'
|
||||
@@ -1,8 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
TokenBlacklistView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
# JWT tokens
|
||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
|
||||
{% if comment_out_api_url %}# {% endif %}path("api/", include("{{ api_module }}.urls")),
|
||||
]
|
||||
|
||||
# Don't show schema in production
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
|
||||
]
|
||||
@@ -1,31 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
TokenBlacklistView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
# JWT tokens
|
||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
|
||||
# App URLs
|
||||
{% if comment_out_api_url %}# {% endif %} {% for app in app_names %} path("", include("{{ app }}.urls")),
|
||||
{% endfor %}]
|
||||
|
||||
# Don't show schema in production
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
|
||||
]
|
||||
@@ -1,7 +0,0 @@
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "api"
|
||||
|
||||
urlpatterns = [
|
||||
path("v1/", include("api.v1.urls")),
|
||||
]
|
||||
@@ -1,7 +0,0 @@
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "v1"
|
||||
|
||||
urlpatterns = [
|
||||
path("users/", include("apps.users.urls")),
|
||||
]
|
||||
@@ -1,8 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class {{ model_class_name }}(models.Model):
|
||||
"""Generated model for {{ app_name }} app."""
|
||||
|
||||
# Add fields here
|
||||
pass
|
||||
@@ -1,9 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
|
||||
|
||||
class {{ model_class_name }}Serializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = {{ model_class_name }}
|
||||
fields = "__all__"
|
||||
@@ -1,10 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
|
||||
|
||||
def get_{{ app_name }}_by_id(object_id: int) -> Optional[{{ model_class_name }}]:
|
||||
try:
|
||||
return {{ model_class_name }}.objects.get(id=object_id)
|
||||
except {{ model_class_name }}.DoesNotExist:
|
||||
return None
|
||||
@@ -1,12 +0,0 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_{{ app_name }}_list_empty():
|
||||
client = APIClient()
|
||||
url = reverse("v1:{{ app_name }}-list")
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
@@ -1,14 +0,0 @@
|
||||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from {{ app_module }}.views.{{ app_name }}_view import {{ model_class_name }}ViewSet
|
||||
|
||||
|
||||
app_name = "{{ app_name }}"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"{{ app_name }}", {{ model_class_name }}ViewSet, basename="{{ app_name }}")
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
@@ -1,9 +0,0 @@
|
||||
from rest_framework import viewsets
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
from {{ app_module }}.serializers.{{ app_name }}_serializer import {{ model_class_name }}Serializer
|
||||
|
||||
|
||||
class {{ model_class_name }}ViewSet(viewsets.ModelViewSet):
|
||||
queryset = {{ model_class_name }}.objects.all()
|
||||
serializer_class = {{ model_class_name }}Serializer
|
||||
@@ -1,9 +0,0 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom user model extending Django's AbstractUser."""
|
||||
|
||||
# Add custom fields here if needed
|
||||
pass
|
||||
@@ -1,9 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.users.models.user import User
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "username", "email", "first_name", "last_name"]
|
||||
@@ -1,10 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from apps.users.models.user import User
|
||||
|
||||
|
||||
def get_user_by_id(user_id: int) -> Optional[User]:
|
||||
try:
|
||||
return User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
@@ -1,12 +0,0 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_users_list_empty():
|
||||
client = APIClient()
|
||||
url = reverse("v1:users-list")
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
@@ -1,14 +0,0 @@
|
||||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from apps.users.views.user_view import UserViewSet
|
||||
|
||||
|
||||
app_name = "users"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"users", UserViewSet, basename="users")
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
@@ -1,13 +0,0 @@
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.users.models.user import User
|
||||
from apps.users.serializers.user_serializer import UserSerializer
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
@@ -1,5 +0,0 @@
|
||||
class ApplicationError(Exception):
|
||||
"""Base application exception."""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
@@ -1,14 +0,0 @@
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestLoggerMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
logger.info("%s %s", request.method, request.path)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
@@ -1,9 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimestampedModel(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -1,9 +0,0 @@
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def success(data=None, status_code=200):
|
||||
return Response({"success": True, "data": data}, status=status_code)
|
||||
|
||||
|
||||
def error(message, status_code=400):
|
||||
return Response({"success": False, "message": message}, status=status_code)
|
||||
@@ -1,8 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.core"
|
||||
@@ -1,4 +0,0 @@
|
||||
from .base import AbstractBaseModel
|
||||
from .mixins import TimestampedModel
|
||||
|
||||
__all__ = ["AbstractBaseModel", "TimestampedModel"]
|
||||
@@ -1,13 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AbstractBaseModel(models.Model):
|
||||
"""Abstract base model with common fields."""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -1,13 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimestampedModel(models.Model):
|
||||
"""Mixin for models that need timestamp fields."""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -1,3 +0,0 @@
|
||||
from .responses import success, error
|
||||
|
||||
__all__ = ["success", "error"]
|
||||
@@ -1,13 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def success(data=None, status_code=200):
|
||||
"""Return a successful response."""
|
||||
return Response({"success": True, "data": data}, status=status_code)
|
||||
|
||||
|
||||
def error(message, status_code=400):
|
||||
"""Return an error response."""
|
||||
return Response({"success": False, "message": message}, status=status_code)
|
||||
@@ -1,32 +0,0 @@
|
||||
# Django settings
|
||||
DJANGO_SETTINGS_MODULE={{ project_name }}.settings.development
|
||||
SECRET_KEY= your-secret-key
|
||||
ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
|
||||
|
||||
{% if use_database_url %}
|
||||
# Database URL (recommended for production)
|
||||
{% if database_type == 'mysql' %}
|
||||
DATABASE_URL=mysql://user:password@localhost:3306/{{ project_name }}
|
||||
{% else %}
|
||||
DATABASE_URL=postgres://user:password@localhost:5432/{{ project_name }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
# Database (individual parameters)
|
||||
DB_NAME=
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
{% endif %}
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=http://frontend.your-domain.com
|
||||
|
||||
# Email settings
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=
|
||||
SERVER_EMAIL=
|
||||
Whitespace-only changes.
@@ -1,20 +0,0 @@
|
||||
# Procfile for {{ project_name }}
|
||||
# Used for deploying to PaaS platforms like Heroku, Railway, Render, etc.
|
||||
|
||||
# Web server process (using Gunicorn)
|
||||
# Adjust workers based on your needs: (2 x CPU cores) + 1
|
||||
web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info
|
||||
|
||||
# Alternative web server configurations (uncomment to use):
|
||||
# web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120
|
||||
# web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120
|
||||
|
||||
# Release task (runs migrations before deployment)
|
||||
release: python manage.py migrate --noinput && python manage.py collectstatic --noinput
|
||||
|
||||
# Uncomment below to run scheduled tasks (if using Celery)
|
||||
# worker: celery -A {{ project_name }} worker --loglevel=info
|
||||
# beat: celery -A {{ project_name }} beat --loglevel=info
|
||||
|
||||
# Development server (for local testing with Foreman/Overmind)
|
||||
# web: python manage.py runserver 0.0.0.0:$PORT
|
||||
@@ -1 +0,0 @@
|
||||
python-{{ python_version }}
|
||||
@@ -1,302 +0,0 @@
|
||||
"""
|
||||
Shared utility functions.
|
||||
Contains common operations used across multiple managers.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
from djinit.services.templates import template_engine
|
||||
from djinit.ui.console import UIFormatter
|
||||
|
||||
|
||||
def format_file(filename: str) -> None:
|
||||
"""Format Python file using Ruff formatter."""
|
||||
subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True)
|
||||
|
||||
|
||||
def create_file_with_content(filename: str, content: str, success_message: str, should_format: bool = False) -> None:
|
||||
from djinit.utils.exceptions import FileError
|
||||
|
||||
try:
|
||||
with open(filename, "w") as file:
|
||||
file.write(content)
|
||||
except OSError as e:
|
||||
raise FileError(f"Failed to write file: {filename}", details=str(e)) from e
|
||||
|
||||
if should_format:
|
||||
format_file(filename)
|
||||
|
||||
UIFormatter.print_success(success_message)
|
||||
|
||||
|
||||
def create_file_from_template(
|
||||
file_path: str,
|
||||
template_path: str,
|
||||
context: dict,
|
||||
success_message: str,
|
||||
should_format: bool = True,
|
||||
) -> None:
|
||||
try:
|
||||
content = template_engine.render_template(template_path, context)
|
||||
except Exception as e:
|
||||
from djinit.utils.exceptions import TemplateError
|
||||
|
||||
raise TemplateError(f"Failed to render template: {template_path}", details=str(e)) from e
|
||||
|
||||
create_file_with_content(file_path, content, success_message, should_format=should_format)
|
||||
|
||||
|
||||
def create_init_file(directory: str, success_message: str) -> None:
|
||||
init_path = os.path.join(directory, "__init__.py")
|
||||
create_file_from_template(init_path, "project/init.j2", {}, success_message, should_format=False)
|
||||
|
||||
|
||||
def create_directory_with_init(dir_path: str, message: str) -> None:
|
||||
"""Create directory and __init__.py file."""
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
create_init_file(dir_path, message)
|
||||
|
||||
|
||||
def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None:
|
||||
"""Create multiple files from templates.
|
||||
|
||||
Args:
|
||||
base_dir: Base directory where files will be created
|
||||
files: List of tuples (filename, template_path, context)
|
||||
prefix: Optional prefix for success message
|
||||
should_format: Whether to format the created files
|
||||
"""
|
||||
for file_spec in files:
|
||||
if len(file_spec) == 3:
|
||||
filename, template_path, context = file_spec
|
||||
else:
|
||||
# Support (filename, template_path) format with empty context
|
||||
filename, template_path = file_spec
|
||||
context = {}
|
||||
filepath = os.path.join(base_dir, filename)
|
||||
message = f"Created {prefix}{filename}" if prefix else f"Created {filename}"
|
||||
create_file_from_template(filepath, template_path, context, message, should_format=should_format)
|
||||
|
||||
|
||||
def get_package_name(project_dir: str) -> str:
|
||||
"""Get package name, defaulting to 'backend' if project_dir is '.' or empty."""
|
||||
return "backend" if project_dir == "." or not project_dir else project_dir
|
||||
|
||||
|
||||
@contextmanager
|
||||
def change_cwd(path: str):
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(path)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
|
||||
def _parse_app_entry_from_line(line: str) -> str | None:
|
||||
"""Extract app name from a line in USER_DEFINED_APPS section."""
|
||||
line_stripped = line.strip().strip(",").strip()
|
||||
for quote in ('"', "'"):
|
||||
if line_stripped.startswith(quote) and line_stripped.endswith(quote):
|
||||
return line_stripped.strip(quote)
|
||||
return None
|
||||
|
||||
|
||||
def _is_user_apps_section_start(line: str) -> bool:
|
||||
"""Check if line starts USER_DEFINED_APPS section."""
|
||||
return "USER_DEFINED_APPS" in line and "=" in line
|
||||
|
||||
|
||||
def _iterate_user_apps_lines(content: str):
|
||||
"""Generator that yields lines within USER_DEFINED_APPS section."""
|
||||
lines = content.split("\n")
|
||||
in_user_apps = False
|
||||
|
||||
for line in lines:
|
||||
if _is_user_apps_section_start(line):
|
||||
in_user_apps = True
|
||||
elif in_user_apps and line.strip() == "]":
|
||||
break
|
||||
elif in_user_apps:
|
||||
yield line
|
||||
|
||||
|
||||
def extract_existing_apps(content: str) -> set:
|
||||
existing_apps = set()
|
||||
for line in _iterate_user_apps_lines(content):
|
||||
app_name = _parse_app_entry_from_line(line)
|
||||
if app_name:
|
||||
existing_apps.add(app_name)
|
||||
return existing_apps
|
||||
|
||||
|
||||
def format_app_entries(apps: list) -> list:
|
||||
return [f' "{app}",' for app in apps]
|
||||
|
||||
|
||||
def is_app_section_start(line: str) -> bool:
|
||||
return "=" in line and any(keyword in line for keyword in ("BUILT_IN_APPS", "THIRD_PARTY_APPS", "INSTALLED_APPS"))
|
||||
|
||||
|
||||
def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool:
|
||||
for next_idx in range(current_idx + 1, min(current_idx + 3, len(lines))):
|
||||
next_line = lines[next_idx].strip()
|
||||
if next_line and "BUILT_IN_APPS" in next_line:
|
||||
return True
|
||||
return True # Default to True if we're inside USER_DEFINED_APPS
|
||||
|
||||
|
||||
def split_bracket_line(line: str, apps_to_add: list) -> list:
|
||||
bracket_pos = line.rfind("]")
|
||||
before_bracket = line[:bracket_pos].rstrip() if bracket_pos > 0 else line.rstrip()
|
||||
after_bracket = line[bracket_pos:] if bracket_pos > 0 else "]"
|
||||
|
||||
result = [before_bracket]
|
||||
result.extend(format_app_entries(apps_to_add))
|
||||
result.append(after_bracket)
|
||||
return result
|
||||
|
||||
|
||||
def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str:
|
||||
lines = content.split("\n")
|
||||
new_lines = []
|
||||
in_user_apps = False
|
||||
apps_added = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if _is_user_apps_section_start(line) and not apps_added:
|
||||
in_user_apps = True
|
||||
|
||||
if "]" in line:
|
||||
new_lines.extend(split_bracket_line(line, apps_to_add))
|
||||
apps_added = True
|
||||
in_user_apps = False
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
elif in_user_apps and line.strip() == "]" and not apps_added:
|
||||
if is_user_apps_closing_bracket(lines, i):
|
||||
new_lines.extend(format_app_entries(apps_to_add))
|
||||
new_lines.append(line)
|
||||
apps_added = True
|
||||
in_user_apps = False
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
elif in_user_apps and is_app_section_start(line) and not apps_added:
|
||||
new_lines.extend(format_app_entries(apps_to_add))
|
||||
new_lines.append("]")
|
||||
new_lines.append(line)
|
||||
apps_added = True
|
||||
in_user_apps = False
|
||||
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if not apps_added:
|
||||
UIFormatter.print_error("Could not find or update USER_DEFINED_APPS section in base.py")
|
||||
return None
|
||||
|
||||
return "\n".join(new_lines)
|
||||
|
||||
|
||||
def calculate_app_module_paths(app_names: list, metadata: dict) -> list:
|
||||
nested = bool(metadata.get("nested_apps"))
|
||||
nested_dir_name = metadata.get("nested_dir") if nested else None
|
||||
return [calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names]
|
||||
|
||||
|
||||
def calculate_app_module_path(app_name: str, nested: bool, nested_dir: str | None) -> str:
|
||||
return f"{nested_dir}.{app_name}" if nested and nested_dir else app_name
|
||||
|
||||
|
||||
def is_django_project(directory: str = None) -> bool:
|
||||
if directory is None:
|
||||
directory = os.getcwd()
|
||||
manage_py_path = os.path.join(directory, "manage.py")
|
||||
return os.path.exists(manage_py_path)
|
||||
|
||||
|
||||
def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]:
|
||||
if search_dir is None:
|
||||
search_dir = os.getcwd()
|
||||
|
||||
for item in os.listdir(search_dir):
|
||||
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
|
||||
candidate = os.path.join(search_dir, item)
|
||||
base_py = os.path.join(candidate, "settings", "base.py")
|
||||
if os.path.exists(base_py):
|
||||
return candidate, base_py
|
||||
return None, None
|
||||
|
||||
|
||||
def find_settings_path(search_dir: str = None) -> str | None:
|
||||
project_dir, settings_base_path = find_project_dir(search_dir)
|
||||
if settings_base_path:
|
||||
return os.path.dirname(settings_base_path)
|
||||
return None
|
||||
|
||||
|
||||
def get_base_settings_path(project_root: str, project_name: str) -> str:
|
||||
settings_dir = os.path.join(project_root, project_name, "settings")
|
||||
return os.path.join(settings_dir, "base.py")
|
||||
|
||||
|
||||
def read_base_settings(project_root: str, project_name: str) -> str | None:
|
||||
base_settings_path = get_base_settings_path(project_root, project_name)
|
||||
if not os.path.exists(base_settings_path):
|
||||
return None
|
||||
|
||||
with open(base_settings_path) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def detect_nested_structure_from_settings(
|
||||
base_settings_path: str, search_dir: str = None
|
||||
) -> tuple[bool, str | None, str]:
|
||||
if search_dir is None:
|
||||
search_dir = os.getcwd()
|
||||
|
||||
if not os.path.exists(base_settings_path):
|
||||
return False, None, search_dir
|
||||
|
||||
try:
|
||||
with open(base_settings_path) as f:
|
||||
base_content = f.read()
|
||||
|
||||
for line in _iterate_user_apps_lines(base_content):
|
||||
app_label = _parse_app_entry_from_line(line)
|
||||
if app_label and "." in app_label:
|
||||
nested_dir_name = app_label.split(".", 1)[0]
|
||||
nested_path = os.path.join(search_dir, nested_dir_name)
|
||||
if os.path.isdir(nested_path):
|
||||
return True, nested_dir_name, nested_path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False, None, search_dir
|
||||
|
||||
|
||||
def get_djinit_config(project_root: str = None) -> dict | None:
|
||||
"""Read .djinit configuration file."""
|
||||
import json
|
||||
|
||||
if project_root is None:
|
||||
project_root = os.getcwd()
|
||||
|
||||
config_path = os.path.join(project_root, ".djinit")
|
||||
if not os.path.exists(config_path):
|
||||
# Try finding it in parent directories
|
||||
parent = os.path.dirname(project_root)
|
||||
if parent and parent != project_root:
|
||||
return get_djinit_config(parent)
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
@@ -1,89 +0,0 @@
|
||||
"""
|
||||
Django helper module that replicates Django's startproject and startapp functionality.
|
||||
|
||||
This module provides functions to create Django projects and apps without requiring Django.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.core.config import DJANGO_VERSION
|
||||
from djinit.utils.common import (
|
||||
create_directory_with_init,
|
||||
create_file_from_template,
|
||||
create_files_from_templates,
|
||||
create_init_file,
|
||||
)
|
||||
|
||||
|
||||
class DjangoHelper:
|
||||
DJANGO_VERSION = DJANGO_VERSION
|
||||
|
||||
@staticmethod
|
||||
def startproject(project_name: str, directory: str, unified: bool = False) -> None:
|
||||
try:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
manage_py_path = os.path.join(directory, "manage.py")
|
||||
create_file_from_template(manage_py_path, "project/manage_py.j2", {}, "Created manage.py")
|
||||
os.chmod(manage_py_path, 0o755)
|
||||
|
||||
if unified:
|
||||
return
|
||||
|
||||
project_config_dir = os.path.join(directory, project_name)
|
||||
create_directory_with_init(project_config_dir, f"Created {project_name}/__init__.py")
|
||||
|
||||
settings_dir = os.path.join(project_config_dir, "settings")
|
||||
create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py")
|
||||
|
||||
base_context = {"project_name": project_name, "app_names": []}
|
||||
settings_files = [
|
||||
("base.py", "config/settings/base.j2", base_context),
|
||||
("development.py", "config/settings/development.j2", {}),
|
||||
("production.py", "config/settings/production.j2", {}),
|
||||
]
|
||||
create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/")
|
||||
|
||||
urls_context = {"project_name": project_name, "django_version": DjangoHelper.DJANGO_VERSION}
|
||||
project_files = [
|
||||
("urls.py", "config/urls/base.j2", urls_context),
|
||||
("wsgi.py", "config/wsgi.j2", {}),
|
||||
("asgi.py", "config/asgi.j2", {}),
|
||||
]
|
||||
create_files_from_templates(project_config_dir, project_files, f"{project_name}/")
|
||||
|
||||
except Exception as e:
|
||||
from djinit.utils.exceptions import DjinitError
|
||||
|
||||
raise DjinitError(f"Error creating Django project: {e}", details=str(e)) from e
|
||||
|
||||
@staticmethod
|
||||
def startapp(app_name: str, directory: str) -> None:
|
||||
try:
|
||||
app_dir = os.path.join(directory, app_name)
|
||||
os.makedirs(app_dir, exist_ok=True)
|
||||
|
||||
context = {"app_name": app_name, "django_version": DjangoHelper.DJANGO_VERSION}
|
||||
|
||||
create_init_file(app_dir, f"Created {app_name}/__init__.py")
|
||||
|
||||
app_files = [
|
||||
("apps.py", "components/apps.j2", context),
|
||||
("models.py", "components/models.j2", context),
|
||||
("views.py", "components/views.j2", context),
|
||||
("admin.py", "components/admin.j2", context),
|
||||
("urls.py", "components/urls.j2", context),
|
||||
("serializers.py", "components/serializers.j2", context),
|
||||
("routes.py", "components/routes.j2", context),
|
||||
("tests.py", "components/tests.j2", context),
|
||||
]
|
||||
|
||||
create_files_from_templates(app_dir, app_files, f"{app_name}/")
|
||||
|
||||
migrations_dir = os.path.join(app_dir, "migrations")
|
||||
create_directory_with_init(migrations_dir, f"Created {app_name}/migrations/__init__.py")
|
||||
|
||||
except Exception as e:
|
||||
from djinit.utils.exceptions import DjinitError
|
||||
|
||||
raise DjinitError(f"Error creating Django app: {e}", details=str(e)) from e
|
||||
+5
-5
@@ -3,11 +3,11 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["djinit"]
|
||||
packages = ["src/djinit"]
|
||||
|
||||
[project]
|
||||
name = "djinitx"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
description = "A CLI tool to setup Django project"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -48,16 +48,16 @@ classifiers=[
|
||||
]
|
||||
dependencies = [
|
||||
"rich>=14.2.0",
|
||||
"jinja2>=3.1.0",
|
||||
"questionary>=2.0.1",
|
||||
"typer>=0.21.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/S4NKALP/djinit"
|
||||
|
||||
[project.scripts]
|
||||
djinit = "djinit.generator:main"
|
||||
dj = "djinit.generator:main"
|
||||
djinit = "djinit.cli:app"
|
||||
dj = "djinit.cli:app"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
jinja2==3.1.6
|
||||
questionary==2.1.1
|
||||
rich==14.2.0
|
||||
rich==15.0.0
|
||||
# dev
|
||||
ruff==0.14.9
|
||||
ruff==0.15.11
|
||||
pre-commit==4.5.1
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
line-length = 120
|
||||
target-version = "py38"
|
||||
exclude = ["django-new"]
|
||||
|
||||
[lint]
|
||||
select = [
|
||||
|
||||
@@ -2,26 +2,25 @@ from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name="djinitx",
|
||||
version="0.3.3",
|
||||
version="0.3.4",
|
||||
description="A CLI tool to setup Django projects",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
author="Sankalp Tharu",
|
||||
author_email="sankalptharu50028@gmail.com",
|
||||
url="https://github.com/S4NKALP/djinit",
|
||||
packages=find_packages(include=["djinit", "djinit.*"]),
|
||||
packages=find_packages(where="src"),
|
||||
package_dir={"": "src"},
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
"ruff>=0.14.2",
|
||||
"click>=8.3.0",
|
||||
"rich>=14.2.0",
|
||||
"jinja2>=3.1.0",
|
||||
"questionary>=2.0.1",
|
||||
"typer>=0.21.1",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"djinit=djinit.generator:main",
|
||||
"dj=djinit.generator:main",
|
||||
"djinit=djinit.cli:app",
|
||||
"dj=djinit.cli:app",
|
||||
]
|
||||
},
|
||||
classifiers=[
|
||||
@@ -55,7 +54,7 @@ setup(
|
||||
"django project",
|
||||
"django starter",
|
||||
],
|
||||
python_requires=">=3.13",
|
||||
python_requires=">=3.10",
|
||||
project_urls={
|
||||
"Bug Reports": "https://github.com/S4NKALP/djinit/issues",
|
||||
"Source": "https://github.com/S4NKALP/djinit",
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,158 @@
|
||||
import logging
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from djinit.creators.app import AppCreator
|
||||
from djinit.creators.setup import SetupCreator
|
||||
from djinit.ui.console import UIColors, UIFormatter, console
|
||||
from djinit.ui.input import get_user_input
|
||||
from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys
|
||||
from djinit.utils.validators import validate_app_name
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = typer.Typer(
|
||||
help="djinit - A CLI tool to set up Django projects with best practices.",
|
||||
add_completion=False,
|
||||
no_args_is_help=False,
|
||||
)
|
||||
|
||||
|
||||
def version_callback(value: bool):
|
||||
if value:
|
||||
from importlib.metadata import version
|
||||
|
||||
try:
|
||||
typer.echo(f"djinit v{version('djinitx')}")
|
||||
except Exception:
|
||||
typer.echo("djinit (version unknown)")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def main(
|
||||
ctx: typer.Context,
|
||||
version: Annotated[
|
||||
Optional[bool],
|
||||
typer.Option("--version", "-v", callback=version_callback, is_eager=True, help="Show the version and exit."),
|
||||
] = None,
|
||||
):
|
||||
"""
|
||||
djinit: Django Project Initializer
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
setup()
|
||||
|
||||
|
||||
@app.command("secret")
|
||||
def secret(
|
||||
count: Annotated[int, typer.Option("--count", "-c", help="Number of keys to generate")] = 3,
|
||||
length: Annotated[int, typer.Option("--length", "-l", help="Length of each secret key")] = 50,
|
||||
):
|
||||
"""
|
||||
Generate Django secret keys.
|
||||
"""
|
||||
keys = generate_multiple_keys(count, length)
|
||||
UIFormatter.print_info("")
|
||||
display_secret_keys(keys)
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT)
|
||||
instructions.append("1. Copy the appropriate secret key for your environment\n")
|
||||
instructions.append("2. Add it to your .env file:\n")
|
||||
instructions.append(" SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
|
||||
instructions.append("3. Or set it as an environment variable:\n")
|
||||
instructions.append(" export SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
|
||||
instructions.append("4. Never commit secret keys to version control!\n", style=UIColors.WARNING)
|
||||
|
||||
console.print(Panel(instructions, title="💡 How to Use", border_style="blue"))
|
||||
console.print()
|
||||
|
||||
|
||||
@app.command("app")
|
||||
def create_app(app_names: Annotated[list[str], typer.Argument(help="Name(s) of the Django app(s) to create")]):
|
||||
"""
|
||||
Create one or more Django apps.
|
||||
"""
|
||||
UIFormatter.print_info("")
|
||||
UIFormatter.print_header("Django App Creation")
|
||||
UIFormatter.print_info("")
|
||||
|
||||
# Process app names (handle commma separation if passed as single arg)
|
||||
processed_names = []
|
||||
for name in app_names:
|
||||
parts = [p.strip() for p in name.split(",") if p.strip()]
|
||||
processed_names.extend(parts)
|
||||
|
||||
processed_names = list(dict.fromkeys(processed_names)) # Dedup
|
||||
|
||||
if not processed_names:
|
||||
UIFormatter.print_error("App name cannot be empty")
|
||||
raise typer.Exit(1)
|
||||
|
||||
for name in processed_names:
|
||||
is_valid, error_msg = validate_app_name(name)
|
||||
if not is_valid:
|
||||
UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
any_failure = False
|
||||
for name in processed_names:
|
||||
creator = AppCreator(name)
|
||||
success = creator.create_app()
|
||||
if not success:
|
||||
UIFormatter.print_error(f"Failed to create Django app '{name}'")
|
||||
any_failure = True
|
||||
break # Stop on failure? Or continue? Original code broke loop on failure.
|
||||
|
||||
if any_failure:
|
||||
raise typer.Exit(1)
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT)
|
||||
instructions.append("1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n")
|
||||
instructions.append("2. Create your models in each app's models.py\n")
|
||||
instructions.append("3. Run migrations: just makemigrations\n")
|
||||
instructions.append("4. Apply migrations: just migrate\n")
|
||||
instructions.append("5. Create views, serializers and routes(URLs) for your app(s)\n")
|
||||
|
||||
console.print(Panel(instructions, title="💡 What's Next", border_style="green"))
|
||||
UIFormatter.print_info("")
|
||||
|
||||
|
||||
@app.command("setup")
|
||||
def setup():
|
||||
"""
|
||||
Interactively set up a new Django project.
|
||||
"""
|
||||
UIFormatter.print_header("Django Project Setup")
|
||||
|
||||
project_dir, project_name, primary_app, app_names, metadata = get_user_input()
|
||||
|
||||
orchestrator = SetupCreator(project_dir, project_name, primary_app, app_names, metadata)
|
||||
success = orchestrator.create()
|
||||
|
||||
if success:
|
||||
UIFormatter.print_info("")
|
||||
UIFormatter.print_header("Setup Complete!")
|
||||
console.print(f"[bold green]Project '{project_name}' has been successfully created![/bold green]")
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT)
|
||||
instructions.append(f"1. cd {project_dir}\n")
|
||||
instructions.append("2. Install dependencies: pip install -r requirements.txt\n")
|
||||
instructions.append("3. Run migrations: just migrate (or python manage.py migrate)\n")
|
||||
instructions.append("4. Start server: just run (or python manage.py runserver)\n")
|
||||
|
||||
console.print(Panel(instructions, title="Get Started", border_style="green"))
|
||||
else:
|
||||
UIFormatter.print_error("Project setup failed. Please check the logs above.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
File renamed without changes.
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Base classes for the djinit architecture.
|
||||
Defines the common interface for services, commands, and utilities.
|
||||
"""
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import Any
|
||||
|
||||
|
||||
class BaseObject:
|
||||
"""
|
||||
Base object for all djinit classes.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class BaseService(BaseObject):
|
||||
"""
|
||||
Base class for all services.
|
||||
Services handle business logic and orchestration.
|
||||
"""
|
||||
|
||||
def __init__(self, project_root: str = None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.project_root = project_root
|
||||
|
||||
|
||||
class BaseCommand(BaseObject):
|
||||
"""
|
||||
Base class for all CLI commands.
|
||||
Commands handle user input and invoke services.
|
||||
"""
|
||||
|
||||
def __init__(self, args: Any, *args_list, **kwargs):
|
||||
super().__init__(*args_list, **kwargs)
|
||||
self.args = args
|
||||
|
||||
@abstractmethod
|
||||
def execute(self) -> None:
|
||||
"""Execute the command."""
|
||||
pass
|
||||
|
||||
|
||||
class BaseUtils(BaseObject):
|
||||
"""
|
||||
Base class for utility collections.
|
||||
Utilities provide helper functions.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -5,7 +5,7 @@ Core configuration and constants for djinit.
|
||||
import sys
|
||||
|
||||
# Django version used for project generation
|
||||
DJANGO_VERSION = "5.2"
|
||||
DJANGO_VERSION = "6.0.1"
|
||||
|
||||
# Reserved names that cannot be used for projects or apps
|
||||
DJANGO_RESERVED = {"django", "test", "site-packages", "admin"}
|
||||
@@ -0,0 +1,215 @@
|
||||
import re
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
class InFileLogicParser:
|
||||
"""
|
||||
A lightweight template parser that uses comment-style markers for logic
|
||||
and [[ variable ]] for substitutions.
|
||||
"""
|
||||
|
||||
def __init__(self, context: Dict[str, Any] = None):
|
||||
self.context = context or {}
|
||||
|
||||
def _get_value(self, key: str) -> str:
|
||||
"""
|
||||
Supports nested/dotted access like [[ user.name ]] or [[ settings["DEBUG"] ]].
|
||||
For simplicity, it uses eval in the provided context for complex expressions,
|
||||
but falls back to safe dictionary lookup for simple keys.
|
||||
"""
|
||||
key = key.strip()
|
||||
try:
|
||||
# Try evaluating the expression in the context
|
||||
return str(eval(key, {"__builtins__": {}}, self.context))
|
||||
except (NameError, SyntaxError, KeyError, TypeError, AttributeError):
|
||||
# Fallback for common patterns or just return the key as is if it fails
|
||||
return f"[[ {key} ]]"
|
||||
|
||||
def render(self, template_text: str, context: Dict[str, Any] = None) -> str:
|
||||
if context is not None:
|
||||
self.context = context
|
||||
|
||||
final_lines = []
|
||||
# Stack stores boolean results of nested IF blocks
|
||||
# Each element is (current_block_result, has_any_true_branch_executed)
|
||||
stack: List[List[bool]] = []
|
||||
|
||||
lines = template_text.splitlines()
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
stripped = line.strip()
|
||||
|
||||
# Handle @IF
|
||||
if stripped.startswith("# @IF "):
|
||||
expr = stripped[6:].strip()
|
||||
try:
|
||||
result = bool(eval(expr, {"__builtins__": {}}, self.context))
|
||||
except Exception:
|
||||
result = False
|
||||
stack.append([result, result])
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Handle @ELSEIF
|
||||
elif stripped.startswith("# @ELSEIF "):
|
||||
if not stack:
|
||||
final_lines.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
expr = stripped[9:].strip()
|
||||
current_stack = stack[-1]
|
||||
|
||||
if current_stack[1]:
|
||||
current_stack[0] = False
|
||||
else:
|
||||
try:
|
||||
result = bool(eval(expr, {"__builtins__": {}}, self.context))
|
||||
except Exception:
|
||||
result = False
|
||||
current_stack[0] = result
|
||||
if result:
|
||||
current_stack[1] = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Handle @ELSE
|
||||
elif stripped.startswith("# @ELSE"):
|
||||
if not stack:
|
||||
final_lines.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
current_stack = stack[-1]
|
||||
if current_stack[1]:
|
||||
current_stack[0] = False
|
||||
else:
|
||||
current_stack[0] = True
|
||||
current_stack[1] = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Handle @ENDIF
|
||||
elif stripped.startswith("# @ENDIF"):
|
||||
if stack:
|
||||
stack.pop()
|
||||
else:
|
||||
final_lines.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Handle @LOOP
|
||||
elif stripped.startswith("# @LOOP "):
|
||||
if stack and not all(s[0] for s in stack):
|
||||
# Skip the entire loop block if inside a false conditional
|
||||
loop_depth = 1
|
||||
i += 1
|
||||
while i < len(lines) and loop_depth > 0:
|
||||
if lines[i].strip().startswith("# @LOOP "):
|
||||
loop_depth += 1
|
||||
elif lines[i].strip().startswith("# @ENDLOOP"):
|
||||
loop_depth -= 1
|
||||
i += 1
|
||||
continue
|
||||
|
||||
parts = stripped[7:].strip().split(" in ")
|
||||
if len(parts) == 2:
|
||||
var_name = parts[0].strip()
|
||||
iterable_name = parts[1].strip()
|
||||
try:
|
||||
iterable = eval(iterable_name, {"__builtins__": {}}, self.context)
|
||||
except Exception:
|
||||
iterable = []
|
||||
|
||||
# Capture loop body
|
||||
loop_body = []
|
||||
i += 1
|
||||
loop_depth = 1
|
||||
while i < len(lines) and loop_depth > 0:
|
||||
if lines[i].strip().startswith("# @LOOP "):
|
||||
loop_depth += 1
|
||||
elif lines[i].strip().startswith("# @ENDLOOP"):
|
||||
loop_depth -= 1
|
||||
|
||||
if loop_depth > 0:
|
||||
loop_body.append(lines[i])
|
||||
i += 1
|
||||
|
||||
if i < len(lines): # Skip the @ENDLOOP line itself
|
||||
i += 1
|
||||
|
||||
# Execute loop
|
||||
if hasattr(iterable, "__iter__"):
|
||||
old_val = self.context.get(var_name)
|
||||
for val in iterable:
|
||||
self.context[var_name] = val
|
||||
rendered_body = self.render("\n".join(loop_body), self.context)
|
||||
final_lines.extend(rendered_body.splitlines())
|
||||
if old_val is not None:
|
||||
self.context[var_name] = old_val
|
||||
else:
|
||||
self.context.pop(var_name, None)
|
||||
continue
|
||||
else:
|
||||
final_lines.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Handle @ENDLOOP (should only be hit if out of sync or error)
|
||||
elif stripped.startswith("# @ENDLOOP"):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Check if we should skip this line based on conditional stack
|
||||
if stack and not all(s[0] for s in stack):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Variable substitution
|
||||
rendered_line = line
|
||||
matches = re.findall(r"\[\[\s*(.*?)\s*\]\]", rendered_line)
|
||||
for match in matches:
|
||||
value = self._get_value(match)
|
||||
# Replace all variations of the variable syntax
|
||||
rendered_line = re.sub(r"\[\[\s*" + re.escape(match) + r"\s*\]\]", value, rendered_line)
|
||||
|
||||
# In-line @IF support
|
||||
# Case 1: content # @IF cond
|
||||
# Case 2: # @IF cond content
|
||||
if "# @IF " in rendered_line:
|
||||
parts = rendered_line.split("# @IF ", 1)
|
||||
pre_content = parts[0].rstrip()
|
||||
rest = parts[1].strip()
|
||||
|
||||
# Split rest into expression and post-content (if any)
|
||||
# This is tricky because the expression might have spaces.
|
||||
# If there's an # @ENDIF on the same line, use it.
|
||||
post_content = ""
|
||||
expr = rest
|
||||
if " # @ENDIF" in rest:
|
||||
expr_parts = rest.split(" # @ENDIF", 1)
|
||||
expr = expr_parts[0].strip()
|
||||
post_content = expr_parts[1].lstrip()
|
||||
elif "# @ENDIF" in rest:
|
||||
expr_parts = rest.split("# @ENDIF", 1)
|
||||
expr = expr_parts[0].strip()
|
||||
post_content = expr_parts[1].lstrip()
|
||||
|
||||
try:
|
||||
if bool(eval(expr, {"__builtins__": {}}, self.context)):
|
||||
# If Case 1, we want pre_content. If Case 2, we want post_content.
|
||||
# Usually, if pre_content is empty (or just whitespace), it's Case 2.
|
||||
if pre_content:
|
||||
final_lines.append(pre_content + post_content)
|
||||
else:
|
||||
final_lines.append(post_content)
|
||||
except Exception:
|
||||
pass
|
||||
i += 1
|
||||
continue
|
||||
|
||||
final_lines.append(rendered_line)
|
||||
i += 1
|
||||
|
||||
return "\n".join(final_lines)
|
||||
@@ -15,6 +15,8 @@ class ProjectMetadata:
|
||||
nested_dir: str | None = None
|
||||
use_database_url: bool = False
|
||||
database_type: str = "postgresql"
|
||||
use_tailwind: bool = False
|
||||
use_htmx: bool = False
|
||||
predefined_structure: bool = False
|
||||
unified_structure: bool = False
|
||||
single_structure: bool = False
|
||||
@@ -29,6 +31,8 @@ class ProjectMetadata:
|
||||
"nested_dir": self.nested_dir,
|
||||
"use_database_url": self.use_database_url,
|
||||
"database_type": self.database_type,
|
||||
"use_tailwind": self.use_tailwind,
|
||||
"use_htmx": self.use_htmx,
|
||||
"predefined_structure": self.predefined_structure,
|
||||
"unified_structure": self.unified_structure,
|
||||
"single_structure": self.single_structure,
|
||||
File renamed without changes.
@@ -0,0 +1,358 @@
|
||||
"""
|
||||
App management for djinit.
|
||||
Handles creation of Django apps and updating settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from djinit.core.base import BaseService
|
||||
from djinit.ui.console import UIFormatter
|
||||
from djinit.utils.common import CommonUtils
|
||||
from djinit.utils.django import DjangoHelper
|
||||
|
||||
|
||||
class AppCreator(BaseService):
|
||||
def __init__(self, app_name: str):
|
||||
super().__init__()
|
||||
self.app_name = app_name
|
||||
self.current_dir = os.getcwd()
|
||||
self.manage_py_path = os.path.join(self.current_dir, "manage.py")
|
||||
self.config = CommonUtils.get_djinit_config(self.current_dir)
|
||||
self._project_structure_cache = None
|
||||
|
||||
def create_app(self) -> bool:
|
||||
if not self._is_django_project():
|
||||
UIFormatter.print_error(
|
||||
"Not in a Django project directory. Please run this command from your Django project root."
|
||||
)
|
||||
return False
|
||||
|
||||
# Check structure type
|
||||
structure_type = self._get_structure_type()
|
||||
|
||||
if structure_type in ["unified", "single"]:
|
||||
UIFormatter.print_warning(
|
||||
f"Creating apps in {structure_type} structure. "
|
||||
"Note: You'll need to manually organize your code within the existing structure."
|
||||
)
|
||||
|
||||
if self._app_exists():
|
||||
UIFormatter.print_error(f"Django app '{self.app_name}' already exists.")
|
||||
return False
|
||||
|
||||
if not self._create_django_app():
|
||||
return False
|
||||
|
||||
if not self._add_to_installed_apps():
|
||||
return False
|
||||
|
||||
# Register URLs based on structure
|
||||
if structure_type == "predefined":
|
||||
self._add_to_api_v1_urls(self.app_name)
|
||||
elif structure_type in ["standard", "single"]:
|
||||
self._add_to_project_urls(self.app_name, structure_type)
|
||||
|
||||
UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!")
|
||||
return True
|
||||
|
||||
def _is_django_project(self) -> bool:
|
||||
return CommonUtils.is_django_project(self.current_dir)
|
||||
|
||||
def _get_structure_type(self) -> str:
|
||||
"""Determine the project structure type."""
|
||||
if self.config:
|
||||
structure = self.config.get("structure", {})
|
||||
if structure.get("unified"):
|
||||
return "unified"
|
||||
elif structure.get("single"):
|
||||
return "single"
|
||||
elif structure.get("predefined"):
|
||||
return "predefined"
|
||||
else:
|
||||
return "standard"
|
||||
|
||||
# Fallback: detect from filesystem
|
||||
if self._is_predefined_structure():
|
||||
return "predefined"
|
||||
if self._is_restricted_structure():
|
||||
# Try to distinguish between unified and single
|
||||
if os.path.isdir(os.path.join(self.current_dir, "apps", "api")):
|
||||
return "unified"
|
||||
return "single"
|
||||
return "standard"
|
||||
|
||||
def _calculate_app_module_path(self, structure_type: str) -> str:
|
||||
"""Calculate the module path for the app based on structure type."""
|
||||
if structure_type == "unified":
|
||||
# Unified structure: apps are not separate, return app name within apps module
|
||||
# But actually in unified, we don't create separate apps - the structure is different
|
||||
# For now, return a path that makes sense: apps.{appname}
|
||||
return f"apps.{self.app_name}"
|
||||
elif structure_type == "single":
|
||||
# Single structure: app is within the single module
|
||||
module_name = self.config.get("project_name") if self.config else self.app_name
|
||||
return f"{module_name}.{self.app_name}"
|
||||
elif structure_type == "predefined":
|
||||
# Predefined: apps are in apps/ directory
|
||||
return f"apps.{self.app_name}"
|
||||
else:
|
||||
# Standard: may be nested or not
|
||||
is_nested, nested_dir, _ = self._get_project_structure()
|
||||
return CommonUtils.calculate_app_module_path(self.app_name, is_nested, nested_dir)
|
||||
|
||||
def _app_exists(self) -> bool:
|
||||
# For predefined structure, check in apps/ directory
|
||||
if self._is_predefined_structure():
|
||||
app_path = os.path.join(self.current_dir, "apps", self.app_name)
|
||||
else:
|
||||
_, _, apps_base_dir = self._get_project_structure()
|
||||
app_path = os.path.join(apps_base_dir, self.app_name)
|
||||
return os.path.exists(app_path)
|
||||
|
||||
def _create_django_app(self) -> bool:
|
||||
if not os.path.exists(self.manage_py_path):
|
||||
UIFormatter.print_error("Could not find manage.py file in project root")
|
||||
return False
|
||||
|
||||
if self._is_predefined_structure():
|
||||
return self._create_predefined_app(os.path.join(self.current_dir, "apps"))
|
||||
|
||||
is_nested, nested_dir, apps_base_dir = self._get_project_structure()
|
||||
|
||||
# Calculate app_module based on project structure
|
||||
if is_nested and nested_dir:
|
||||
# Normalize nested_dir (replace / with . for module path)
|
||||
module_base = nested_dir.replace(os.path.sep, ".")
|
||||
app_module = f"{module_base}.{self.app_name}"
|
||||
else:
|
||||
app_module = self.app_name
|
||||
|
||||
success = DjangoHelper.startapp(self.app_name, apps_base_dir, app_module)
|
||||
if success:
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
|
||||
else:
|
||||
UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'")
|
||||
return success
|
||||
|
||||
def _add_to_installed_apps(self) -> bool:
|
||||
structure_type = self._get_structure_type()
|
||||
|
||||
# Find settings directory based on structure type
|
||||
if structure_type == "unified":
|
||||
settings_path = os.path.join(self.current_dir, "core", "settings")
|
||||
elif structure_type == "single":
|
||||
# For single structure, find the module name from config or detect it
|
||||
module_name = self.config.get("project_name") if self.config else None
|
||||
if not module_name:
|
||||
# Fallback: find settings directory
|
||||
settings_path = CommonUtils.find_settings_path(self.current_dir)
|
||||
else:
|
||||
settings_path = os.path.join(self.current_dir, module_name, "settings")
|
||||
else:
|
||||
# Standard or predefined structure
|
||||
settings_path = CommonUtils.find_settings_path(self.current_dir)
|
||||
|
||||
if not settings_path or not os.path.exists(settings_path):
|
||||
UIFormatter.print_error("Could not find Django settings directory")
|
||||
return False
|
||||
|
||||
base_settings_path = os.path.join(settings_path, "base.py")
|
||||
if not os.path.exists(base_settings_path):
|
||||
UIFormatter.print_error("Could not find base.py settings file")
|
||||
return False
|
||||
|
||||
with open(base_settings_path) as f:
|
||||
content = f.read()
|
||||
|
||||
# Calculate the app module path based on structure
|
||||
app_module_path = self._calculate_app_module_path(structure_type)
|
||||
|
||||
# Use centralized logic for AppConfig path
|
||||
is_nested, nested_dir, _ = self._get_project_structure()
|
||||
nested = structure_type in ["unified", "predefined"] or (structure_type == "standard" and is_nested)
|
||||
|
||||
# Determine the effective nested_dir for config path
|
||||
if structure_type in ["unified", "predefined"]:
|
||||
effective_nested_dir = "apps"
|
||||
elif structure_type == "single":
|
||||
# For single structure, app is inside the project module
|
||||
project_dir, _ = CommonUtils.find_project_dir(self.current_dir)
|
||||
effective_nested_dir = os.path.basename(project_dir) if project_dir else None
|
||||
nested = True
|
||||
else:
|
||||
effective_nested_dir = nested_dir
|
||||
|
||||
full_app_config = CommonUtils.get_full_app_config_path(self.app_name, nested, effective_nested_dir)
|
||||
|
||||
# We'll check if either the module path or the full config path exists
|
||||
# but we'll prefer adding the full config path
|
||||
|
||||
existing_apps = CommonUtils.extract_existing_apps(content)
|
||||
|
||||
# Check if full config is already there
|
||||
if full_app_config in existing_apps:
|
||||
UIFormatter.print_success(f"App '{full_app_config}' already configured in USER_DEFINED_APPS")
|
||||
return True
|
||||
|
||||
# Check if legacy module path is there, if so, upgrade it
|
||||
if app_module_path in existing_apps:
|
||||
UIFormatter.print_info(f"Upgrading app configuration from '{app_module_path}' to '{full_app_config}'...")
|
||||
updated_content = CommonUtils.replace_app_in_user_defined_apps(content, app_module_path, full_app_config)
|
||||
|
||||
if updated_content:
|
||||
with open(base_settings_path, "w") as f:
|
||||
f.write(updated_content)
|
||||
UIFormatter.print_success(f"Upgraded '{app_module_path}' to '{full_app_config}' in USER_DEFINED_APPS")
|
||||
return True
|
||||
else:
|
||||
UIFormatter.print_error(f"Failed to upgrade app '{app_module_path}'")
|
||||
return False
|
||||
|
||||
if "USER_DEFINED_APPS" not in content:
|
||||
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
|
||||
return False
|
||||
|
||||
updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, [full_app_config])
|
||||
if not updated_content:
|
||||
return False
|
||||
|
||||
with open(base_settings_path, "w") as f:
|
||||
f.write(updated_content)
|
||||
|
||||
UIFormatter.print_success(f"Added '{full_app_config}' to USER_DEFINED_APPS in base.py")
|
||||
return True
|
||||
|
||||
def _get_project_structure(self) -> tuple[bool, Optional[str], str]:
|
||||
"""Get project structure with caching to avoid repeated detection."""
|
||||
if self._project_structure_cache is None:
|
||||
if self.config:
|
||||
apps_config = self.config.get("apps", {})
|
||||
nested = apps_config.get("nested", False)
|
||||
nested_dir = apps_config.get("nested_dir")
|
||||
|
||||
# Determine base dir for apps
|
||||
if nested and nested_dir:
|
||||
apps_base_dir = os.path.join(self.current_dir, nested_dir)
|
||||
else:
|
||||
apps_base_dir = self.current_dir
|
||||
|
||||
self._project_structure_cache = (nested, nested_dir, apps_base_dir)
|
||||
else:
|
||||
# Fallback to old detection method
|
||||
project_dir, settings_base_path = CommonUtils.find_project_dir(self.current_dir)
|
||||
|
||||
if project_dir is None:
|
||||
self._project_structure_cache = (False, None, self.current_dir)
|
||||
else:
|
||||
self._project_structure_cache = CommonUtils.detect_nested_structure_from_settings(
|
||||
settings_base_path, self.current_dir
|
||||
)
|
||||
return self._project_structure_cache
|
||||
|
||||
def _is_predefined_structure(self) -> bool:
|
||||
if self.config:
|
||||
return self.config.get("structure", {}).get("predefined", False)
|
||||
|
||||
apps_dir = os.path.join(self.current_dir, "apps")
|
||||
api_dir = os.path.join(self.current_dir, "api")
|
||||
return os.path.isdir(apps_dir) and os.path.isdir(api_dir)
|
||||
|
||||
def _is_restricted_structure(self) -> bool:
|
||||
"""Check if the project structure is Unified or Single Folder."""
|
||||
if self.config:
|
||||
structure = self.config.get("structure", {})
|
||||
return structure.get("unified", False) or structure.get("single", False)
|
||||
|
||||
# Check for Unified structure (apps/api exists)
|
||||
if os.path.isdir(os.path.join(self.current_dir, "apps", "api")):
|
||||
return True
|
||||
|
||||
# Check for Predefined structure (apps/ exists but not apps/api)
|
||||
if os.path.isdir(os.path.join(self.current_dir, "apps")):
|
||||
return False
|
||||
|
||||
# Check for Single Folder structure (models/ and api/ exist in project dir)
|
||||
project_dir, _ = CommonUtils.find_project_dir(self.current_dir)
|
||||
if project_dir:
|
||||
has_models = os.path.isdir(os.path.join(project_dir, "models"))
|
||||
has_api = os.path.isdir(os.path.join(project_dir, "api"))
|
||||
if has_models and has_api:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _create_predefined_app(self, apps_dir: str) -> bool:
|
||||
"""Create an app following the predefined structure with standard Django files."""
|
||||
app_module = f"apps.{self.app_name}"
|
||||
|
||||
# Use DjangoHelper.startapp to create standard Django app structure
|
||||
success = DjangoHelper.startapp(self.app_name, apps_dir, app_module)
|
||||
|
||||
if success:
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' with predefined structure in {apps_dir}")
|
||||
else:
|
||||
UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'")
|
||||
|
||||
return success
|
||||
|
||||
def _add_to_project_urls(self, app_name: str, structure_type: str) -> None:
|
||||
"""Add app URLs to the main project urls.py."""
|
||||
project_dir, settings_base_path = CommonUtils.find_project_dir(self.current_dir)
|
||||
if not project_dir:
|
||||
return
|
||||
|
||||
project_urls = os.path.join(project_dir, "urls.py")
|
||||
if not os.path.exists(project_urls):
|
||||
return
|
||||
|
||||
app_module_path = self._calculate_app_module_path(structure_type)
|
||||
include_stmt = f'path("", include("{app_module_path}.urls")),'
|
||||
|
||||
try:
|
||||
with open(project_urls, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
if include_stmt in content:
|
||||
return
|
||||
|
||||
if "urlpatterns = [" in content:
|
||||
# Find the best place to insert (before the closing bracket of urlpatterns)
|
||||
idx = content.rfind("]")
|
||||
if idx != -1:
|
||||
content = content[:idx] + f" {include_stmt}\n" + content[idx:]
|
||||
|
||||
with open(project_urls, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
UIFormatter.print_success(f"Added '{app_name}' URLs to {os.path.basename(project_urls)}")
|
||||
except Exception as e:
|
||||
UIFormatter.print_warning(f"Could not automatically update {os.path.basename(project_urls)}: {e}")
|
||||
|
||||
def _add_to_api_v1_urls(self, app_name: str) -> None:
|
||||
api_v1_urls = os.path.join(self.current_dir, "api", "v1", "urls.py")
|
||||
if not os.path.exists(api_v1_urls):
|
||||
return
|
||||
|
||||
structure_type = self._get_structure_type()
|
||||
app_module_path = self._calculate_app_module_path(structure_type)
|
||||
|
||||
try:
|
||||
with open(api_v1_urls, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
include_stmt = f'path("{app_name}/", include("{app_module_path}.urls")),'
|
||||
if include_stmt in content:
|
||||
return
|
||||
|
||||
if "urlpatterns = [" in content:
|
||||
idx = content.rfind("]")
|
||||
if idx != -1:
|
||||
content = content[:idx] + f" {include_stmt}\n" + content[idx:]
|
||||
|
||||
with open(api_v1_urls, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
UIFormatter.print_success(f"Added '{app_name}' URLs to api/v1/urls.py")
|
||||
except Exception:
|
||||
return
|
||||
@@ -5,20 +5,14 @@ Handles creation and modification of project files.
|
||||
|
||||
import os
|
||||
|
||||
from djinit.services.templates import template_engine
|
||||
from djinit.utils.common import (
|
||||
calculate_app_module_paths,
|
||||
change_cwd,
|
||||
create_directory_with_init,
|
||||
create_file_from_template,
|
||||
create_file_with_content,
|
||||
get_package_name,
|
||||
)
|
||||
from djinit.core.base import BaseService
|
||||
from djinit.templater import template_engine
|
||||
from djinit.utils.common import CommonUtils
|
||||
|
||||
|
||||
class FileManager:
|
||||
class FileCreator(BaseService):
|
||||
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
|
||||
self.project_root = project_root
|
||||
super().__init__(project_root=project_root)
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
@@ -38,9 +32,9 @@ class FileManager:
|
||||
) -> None:
|
||||
"""Helper to render template and create file."""
|
||||
target_dir = base_dir or self.project_root
|
||||
with change_cwd(target_dir):
|
||||
with CommonUtils.change_cwd(target_dir):
|
||||
content = template_engine.render_template(template_path, context)
|
||||
create_file_with_content(filepath, content, message, should_format=should_format)
|
||||
CommonUtils.create_file_with_content(filepath, content, message, should_format=should_format)
|
||||
|
||||
def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None:
|
||||
"""Helper to create multiple folders and files from a specification dict."""
|
||||
@@ -48,25 +42,34 @@ class FileManager:
|
||||
|
||||
for folder_name, file_templates in folder_specs.items():
|
||||
folder_path = os.path.join(base_dir, folder_name)
|
||||
create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
|
||||
CommonUtils.create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
|
||||
|
||||
for filename, template_path in file_templates:
|
||||
file_path = os.path.join(folder_path, filename)
|
||||
create_file_from_template(file_path, template_path, base_context, f"Created {file_path}")
|
||||
CommonUtils.create_file_from_template(file_path, template_path, base_context, f"Created {file_path}")
|
||||
|
||||
def _create_apps_py(self, app_dir: str, app_name: str, message: str = None, should_format: bool = False) -> None:
|
||||
def _create_apps_py(
|
||||
self, app_dir: str, app_name: str, app_module: str = None, message: str = None, should_format: bool = False
|
||||
) -> None:
|
||||
"""Helper to create apps.py file for an app."""
|
||||
apps_py_path = os.path.join(app_dir, "apps.py")
|
||||
context = {"app_name": app_name} if app_name else {}
|
||||
app_config_name = app_name.title().replace("_", "")
|
||||
context = {
|
||||
"app_name": app_name,
|
||||
"app_config_name": app_config_name,
|
||||
"app_module": app_module or app_name,
|
||||
}
|
||||
msg = message or f"Created {os.path.basename(app_dir)}/apps.py"
|
||||
create_file_from_template(apps_py_path, "components/apps.j2", context, msg, should_format=should_format)
|
||||
CommonUtils.create_file_from_template(
|
||||
apps_py_path, "components/apps.py-tpl", context, msg, should_format=should_format
|
||||
)
|
||||
|
||||
def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None:
|
||||
"""Helper to create multiple subdirectories with __init__.py files."""
|
||||
for subdir in subdirs:
|
||||
subdir_path = os.path.join(base_dir, subdir)
|
||||
message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py"
|
||||
create_directory_with_init(subdir_path, message)
|
||||
CommonUtils.create_directory_with_init(subdir_path, message)
|
||||
|
||||
def _create_settings_package(self, settings_dir: str, base_context: dict, prefix: str) -> None:
|
||||
"""Helper to create settings package files (base, development, production)."""
|
||||
@@ -75,13 +78,27 @@ class FileManager:
|
||||
secret_key = generate_secret_key()
|
||||
dev_context = {"secret_key": secret_key}
|
||||
|
||||
# Prepare context for base.py with full AppConfig paths
|
||||
base_settings_context = base_context.copy()
|
||||
if "app_names" in base_settings_context:
|
||||
nested = bool(self.metadata.get("nested_apps"))
|
||||
nested_dir = self.metadata.get("nested_dir")
|
||||
base_settings_context["app_names"] = [
|
||||
CommonUtils.get_full_app_config_path(name, nested, nested_dir)
|
||||
for name in base_settings_context["app_names"]
|
||||
]
|
||||
|
||||
# Add Tailwind and HTMX to context
|
||||
base_settings_context["use_tailwind"] = self.metadata.get("use_tailwind", False)
|
||||
base_settings_context["use_htmx"] = self.metadata.get("use_htmx", False)
|
||||
|
||||
for filename, context in [
|
||||
("base.py", base_context),
|
||||
("base.py", base_settings_context),
|
||||
("development.py", dev_context),
|
||||
("production.py", base_context),
|
||||
("production.py", base_settings_context),
|
||||
]:
|
||||
filepath = os.path.join(settings_dir, filename)
|
||||
template_path = f"config/settings/{filename.replace('.py', '')}.j2"
|
||||
template_path = f"config/settings/{filename.replace('.py', '')}.py-tpl"
|
||||
self._render_and_create_file(
|
||||
filepath,
|
||||
template_path,
|
||||
@@ -98,14 +115,14 @@ class FileManager:
|
||||
) -> None:
|
||||
"""Helper to create urls.py, wsgi.py, and asgi.py."""
|
||||
|
||||
urls_context = {"api_module": api_module}
|
||||
urls_context = {"url_type": "project", "api_module": api_module, "include_api": True}
|
||||
if comment_out_api:
|
||||
urls_context["comment_out_api_url"] = True
|
||||
|
||||
files = [
|
||||
("urls.py", "config/urls/with_api.j2", urls_context),
|
||||
("wsgi.py", "config/wsgi.j2", {}),
|
||||
("asgi.py", "config/asgi.j2", {}),
|
||||
("urls.py", "config/urls.py-tpl", urls_context),
|
||||
("wsgi.py", "config/wsgi.py-tpl", {"project_name": prefix}),
|
||||
("asgi.py", "config/asgi.py-tpl", {"project_name": prefix}),
|
||||
]
|
||||
|
||||
for filename, template, context in files:
|
||||
@@ -115,16 +132,18 @@ class FileManager:
|
||||
)
|
||||
|
||||
def create_gitignore(self) -> None:
|
||||
self._render_and_create_file(".gitignore", "project/gitignore.j2", {}, "Created .gitignore file")
|
||||
self._render_and_create_file(".gitignore", "project/gitignore-tpl", {}, "Created .gitignore file")
|
||||
|
||||
def create_requirements(self) -> None:
|
||||
context = {
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_htmx": self.metadata.get("use_htmx", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"requirements.txt",
|
||||
"project/requirements.j2",
|
||||
"project/requirements-tpl",
|
||||
context,
|
||||
"Created requirements.txt with Django dependencies",
|
||||
)
|
||||
@@ -137,43 +156,52 @@ class FileManager:
|
||||
"unified_structure": bool(self.metadata.get("unified_structure")),
|
||||
"module_name": self.module_name,
|
||||
}
|
||||
self._render_and_create_file("README.md", "project/readme.j2", context, "Created README.md file")
|
||||
self._render_and_create_file("README.md", "project/readme.md-tpl", context, "Created README.md file")
|
||||
|
||||
def create_env_file(self) -> None:
|
||||
"""Create .env.sample file with environment variables."""
|
||||
context = {
|
||||
"project_name": self.module_name,
|
||||
"project_name": self.module_name, # Use module_name here for settings path
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
".env.sample",
|
||||
"project/env_sample.j2",
|
||||
"project/env_sample-tpl",
|
||||
context,
|
||||
"Created .env.sample file with generated secret key",
|
||||
)
|
||||
|
||||
def create_pyproject_toml(self, metadata: dict) -> None:
|
||||
def create_pyproject(self, metadata: dict) -> None:
|
||||
package_name = metadata.get("package_name", "backend")
|
||||
package_name = get_package_name(package_name)
|
||||
context = {"package_name": package_name, "project_name": self.project_name}
|
||||
package_name = CommonUtils.get_package_name(package_name)
|
||||
context = {
|
||||
"package_name": package_name,
|
||||
"project_name": self.project_name,
|
||||
"module_name": self.module_name,
|
||||
"database_type": metadata.get("database_type", "postgresql"),
|
||||
"use_database_url": metadata.get("use_database_url", True),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"pyproject.toml",
|
||||
"project/pyproject_toml.j2",
|
||||
"project/pyproject.toml-tpl",
|
||||
context,
|
||||
"Created pyproject.toml file for uv",
|
||||
)
|
||||
|
||||
def create_project_urls(self) -> None:
|
||||
"""Create project urls.py using project_urls.j2 template (replaces update_project_urls)."""
|
||||
effective_app_modules = calculate_app_module_paths(self.app_names, self.metadata)
|
||||
"""Create project urls.py using project_config/urls.py-tpl template (replaces update_project_urls)."""
|
||||
effective_app_modules = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
|
||||
context = {
|
||||
"url_type": "project",
|
||||
"project_name": self.project_name,
|
||||
"app_names": effective_app_modules,
|
||||
"include_apps": True,
|
||||
"include_api": True,
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"urls.py",
|
||||
"config/urls/with_apps.j2",
|
||||
"config/urls.py-tpl",
|
||||
context,
|
||||
"Created project urls.py with comprehensive URL configuration",
|
||||
base_dir=self.project_configs,
|
||||
@@ -181,69 +209,53 @@ class FileManager:
|
||||
)
|
||||
|
||||
def create_justfile(self) -> None:
|
||||
context = {"project_name": self.project_name}
|
||||
context = {"project_name": self.project_name, "module_name": self.module_name}
|
||||
self._render_and_create_file(
|
||||
"justfile",
|
||||
"project/justfile.j2",
|
||||
"project/justfile-tpl",
|
||||
context,
|
||||
"Created justfile with Django development tasks",
|
||||
)
|
||||
|
||||
def create_predefined_structure(self) -> None:
|
||||
"""Create predefined structure with folder organization only (no example code)."""
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
|
||||
users_dir = os.path.join(apps_dir, "users")
|
||||
create_directory_with_init(users_dir, "Created apps/users/__init__.py")
|
||||
self._create_apps_py(users_dir, "users")
|
||||
|
||||
users_subfolders = {
|
||||
"models": [("user.py", "presets/predefined/apps/users/models.j2")],
|
||||
"serializers": [("user_serializer.py", "presets/predefined/apps/users/serializers.j2")],
|
||||
"services": [("user_service.py", "presets/predefined/apps/users/services.j2")],
|
||||
"views": [("user_view.py", "presets/predefined/apps/users/views.j2")],
|
||||
"tests": [("test_user_api.py", "presets/predefined/apps/users/tests.j2")],
|
||||
}
|
||||
self._create_files_from_specs(users_dir, users_subfolders, {"app_module": "apps.users"})
|
||||
|
||||
users_urls_path = os.path.join(users_dir, "urls.py")
|
||||
create_file_from_template(
|
||||
users_urls_path,
|
||||
"presets/predefined/apps/users/urls.j2",
|
||||
{"app_module": "apps.users"},
|
||||
"Created apps/users/urls.py",
|
||||
)
|
||||
CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
|
||||
# Create core app with empty subfolders (no example utilities/middleware)
|
||||
core_dir = os.path.join(apps_dir, "core")
|
||||
create_directory_with_init(core_dir, "Created apps/core/__init__.py")
|
||||
CommonUtils.create_directory_with_init(core_dir, "Created apps/core/__init__.py")
|
||||
|
||||
core_subfolders = {
|
||||
"utils": [("responses.py", "presets/predefined/core/utils/responses.j2")],
|
||||
"mixins": [("timestamped_model.py", "presets/predefined/core/mixins/timestamped_model.j2")],
|
||||
"middleware": [("request_logger.py", "presets/predefined/core/middleware/request_logger.j2")],
|
||||
}
|
||||
self._create_files_from_specs(core_dir, core_subfolders, {})
|
||||
|
||||
exceptions_path = os.path.join(core_dir, "exceptions.py")
|
||||
create_file_from_template(
|
||||
exceptions_path, "presets/predefined/core/exceptions.j2", {}, "Created apps/core/exceptions.py"
|
||||
)
|
||||
# Create empty folders for utilities, mixins, middleware (developers add their own)
|
||||
core_subfolders = ["utils", "mixins", "middleware"]
|
||||
self._create_subdirectories_with_init(core_dir, core_subfolders, "apps/core/")
|
||||
|
||||
# Create api directory structure
|
||||
api_dir = os.path.join(self.project_root, "api")
|
||||
create_directory_with_init(api_dir, "Created api/__init__.py")
|
||||
CommonUtils.create_directory_with_init(api_dir, "Created api/__init__.py")
|
||||
api_urls_path = os.path.join(api_dir, "urls.py")
|
||||
create_file_from_template(api_urls_path, "presets/predefined/api/urls.j2", {}, "Created api/urls.py")
|
||||
CommonUtils.create_file_from_template(
|
||||
api_urls_path,
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "api"},
|
||||
"Created api/urls.py",
|
||||
)
|
||||
|
||||
api_v1_dir = os.path.join(api_dir, "v1")
|
||||
create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py")
|
||||
CommonUtils.create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py")
|
||||
api_v1_urls_path = os.path.join(api_v1_dir, "urls.py")
|
||||
create_file_from_template(api_v1_urls_path, "presets/predefined/api/v1/urls.j2", {}, "Created api/v1/urls.py")
|
||||
CommonUtils.create_file_from_template(
|
||||
api_v1_urls_path,
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "api_version", "app_name": "v1", "app_list": self.app_names, "app_module": "apps"},
|
||||
"Created api/v1/urls.py",
|
||||
)
|
||||
|
||||
project_urls_path = os.path.join(self.project_configs, "urls.py")
|
||||
create_file_from_template(
|
||||
CommonUtils.create_file_from_template(
|
||||
project_urls_path,
|
||||
"config/urls/with_api.j2",
|
||||
{"api_module": "api"},
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "project", "api_module": "api", "include_api": True},
|
||||
"Updated project urls.py to include API routes",
|
||||
should_format=True,
|
||||
)
|
||||
@@ -251,10 +263,10 @@ class FileManager:
|
||||
def create_unified_structure(self) -> None:
|
||||
# 1. Create 'core' directory (Project Config)
|
||||
core_dir = os.path.join(self.project_root, "core")
|
||||
create_directory_with_init(core_dir, "Created core/__init__.py")
|
||||
CommonUtils.create_directory_with_init(core_dir, "Created core/__init__.py")
|
||||
|
||||
settings_dir = os.path.join(core_dir, "settings")
|
||||
create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
|
||||
CommonUtils.create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
|
||||
|
||||
# Create settings files
|
||||
base_context = {
|
||||
@@ -270,10 +282,10 @@ class FileManager:
|
||||
|
||||
# 2. Create 'apps' directory (The Main App)
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
|
||||
# Create apps.py for the 'apps' app
|
||||
self._create_apps_py(apps_dir, "apps", "Created apps/apps.py", should_format=True)
|
||||
self._create_apps_py(apps_dir, "apps", app_module="apps", message="Created apps/apps.py", should_format=True)
|
||||
|
||||
# Create sub-components as directories with __init__.py
|
||||
components = ["admin", "models", "serializers", "tests", "urls", "views"]
|
||||
@@ -281,33 +293,34 @@ class FileManager:
|
||||
|
||||
# Create 'api' directory with v1
|
||||
api_dir = os.path.join(apps_dir, "api")
|
||||
create_directory_with_init(api_dir, "Created apps/api/__init__.py")
|
||||
CommonUtils.create_directory_with_init(api_dir, "Created apps/api/__init__.py")
|
||||
|
||||
create_file_from_template(
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(api_dir, "urls.py"),
|
||||
"presets/predefined/api/urls.j2",
|
||||
{},
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "apps.api"},
|
||||
"Created apps/api/urls.py",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
api_v1_dir = os.path.join(api_dir, "v1")
|
||||
create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py")
|
||||
CommonUtils.create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py")
|
||||
|
||||
create_file_from_template(
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(api_v1_dir, "urls.py"),
|
||||
"presets/predefined/api/v1/urls.j2",
|
||||
{},
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "api_version", "app_name": "v1", "app_list": [], "app_module": "apps"},
|
||||
"Created apps/api/v1/urls.py",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def create_single_structure(self) -> None:
|
||||
"""Create single folder structure with minimal files (no example code)."""
|
||||
project_dir = os.path.join(self.project_root, self.module_name)
|
||||
create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py")
|
||||
CommonUtils.create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py")
|
||||
|
||||
settings_dir = os.path.join(project_dir, "settings")
|
||||
create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
|
||||
CommonUtils.create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
|
||||
|
||||
# Create settings files
|
||||
base_context = {
|
||||
@@ -323,42 +336,30 @@ class FileManager:
|
||||
project_dir, self.module_name, api_module=f"{self.module_name}.api", comment_out_api=True
|
||||
)
|
||||
|
||||
# Create component folders with __init__.py only (no example code)
|
||||
components = ["admin", "api", "models", "tests"]
|
||||
self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/")
|
||||
|
||||
create_file_from_template(
|
||||
os.path.join(project_dir, "admin", "__init__.py"),
|
||||
"components/admin.j2",
|
||||
{},
|
||||
f"Created {self.module_name}/admin/__init__.py",
|
||||
)
|
||||
|
||||
create_file_from_template(
|
||||
os.path.join(project_dir, "models", "__init__.py"),
|
||||
"components/models.j2",
|
||||
{},
|
||||
f"Created {self.module_name}/models/__init__.py",
|
||||
)
|
||||
|
||||
create_file_from_template(
|
||||
# Create README files for guidance
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(project_dir, "api", "README.md"),
|
||||
"components/api_readme.j2",
|
||||
"components/api_readme.md-tpl",
|
||||
{"module_name": self.module_name},
|
||||
f"Created {self.module_name}/api/README.md",
|
||||
)
|
||||
|
||||
create_file_from_template(
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(project_dir, "models", "README.md"),
|
||||
"components/models_readme.j2",
|
||||
"components/models_readme.md-tpl",
|
||||
{"module_name": self.module_name},
|
||||
f"Created {self.module_name}/models/README.md",
|
||||
)
|
||||
|
||||
def create_procfile(self) -> None:
|
||||
context = {"project_name": self.project_name}
|
||||
context = {"project_name": self.project_name, "module_name": self.module_name}
|
||||
self._render_and_create_file(
|
||||
"Procfile",
|
||||
"project/procfile.j2",
|
||||
"project/procfile-tpl",
|
||||
context,
|
||||
"Created Procfile with Gunicorn configuration",
|
||||
)
|
||||
@@ -367,7 +368,7 @@ class FileManager:
|
||||
context = {"python_version": "3.13"}
|
||||
self._render_and_create_file(
|
||||
"runtime.txt",
|
||||
"project/runtime_txt.j2",
|
||||
"project/runtime_txt-tpl",
|
||||
context,
|
||||
"Created runtime.txt with Python version specification",
|
||||
)
|
||||
@@ -379,7 +380,7 @@ class FileManager:
|
||||
workflow_file = os.path.join(github_dir, "ci.yml")
|
||||
self._render_and_create_file(
|
||||
workflow_file,
|
||||
"project/ci/github_actions.j2",
|
||||
"project/ci/github_actions-tpl",
|
||||
context,
|
||||
"Created Github Actions workflow (ci.yml)",
|
||||
base_dir=self.project_root,
|
||||
@@ -389,7 +390,7 @@ class FileManager:
|
||||
context = {"project_name": self.project_name, "module_name": self.module_name}
|
||||
self._render_and_create_file(
|
||||
".gitlab-ci.yml",
|
||||
"project/ci/gitlab_ci.j2",
|
||||
"project/ci/gitlab_ci-tpl",
|
||||
context,
|
||||
"Created GitLab CI configuration (.gitlab-ci.yml)",
|
||||
)
|
||||
@@ -418,6 +419,8 @@ class FileManager:
|
||||
"settings": {
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_htmx": self.metadata.get("use_htmx", False),
|
||||
},
|
||||
"cicd": {
|
||||
"github": self.metadata.get("use_github_actions", False),
|
||||
@@ -426,4 +429,6 @@ class FileManager:
|
||||
}
|
||||
|
||||
filepath = os.path.join(self.project_root, ".djinit")
|
||||
create_file_with_content(filepath, json.dumps(config, indent=4), "Created .djinit configuration file")
|
||||
CommonUtils.create_file_with_content(
|
||||
filepath, json.dumps(config, indent=4), "Created .djinit configuration file"
|
||||
)
|
||||
@@ -5,32 +5,29 @@ Handles creation of Django projects and apps.
|
||||
|
||||
import os
|
||||
|
||||
from djinit.core.base import BaseService
|
||||
from djinit.ui.console import UIFormatter
|
||||
from djinit.utils.common import (
|
||||
calculate_app_module_paths,
|
||||
create_directory_with_init,
|
||||
extract_existing_apps,
|
||||
get_base_settings_path,
|
||||
insert_apps_into_user_defined_apps,
|
||||
read_base_settings,
|
||||
)
|
||||
from djinit.utils.common import CommonUtils
|
||||
from djinit.utils.django import DjangoHelper
|
||||
|
||||
|
||||
class ProjectManager:
|
||||
class ProjectCreator(BaseService):
|
||||
def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict):
|
||||
super().__init__(project_root=os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir))
|
||||
self.project_dir = project_dir
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
|
||||
# self.project_root is set by BaseService via super().__init__ but we need to ensure it matches logic
|
||||
# logic was: self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
|
||||
# which I passed to super. So I can remove the line setting self.project_root here.
|
||||
self.module_name = metadata.get("project_module_name") or self.project_name
|
||||
|
||||
def create_project(self) -> None:
|
||||
os.makedirs(self.project_root, exist_ok=True)
|
||||
|
||||
unified = self.metadata.get("unified_structure", False)
|
||||
DjangoHelper.startproject(self.module_name, self.project_root, unified=unified)
|
||||
DjangoHelper.startproject(self.module_name, self.project_root, unified=unified, metadata=self.metadata)
|
||||
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
|
||||
|
||||
def _get_apps_base_dir(self) -> str:
|
||||
@@ -44,39 +41,75 @@ class ProjectManager:
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
if apps_base_dir != self.project_root:
|
||||
create_directory_with_init(apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py")
|
||||
CommonUtils.create_directory_with_init(
|
||||
apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py"
|
||||
)
|
||||
|
||||
for app_name in self.app_names:
|
||||
DjangoHelper.startapp(app_name, apps_base_dir)
|
||||
# Calculate module paths for correct AppConfig generation (e.g. src.users)
|
||||
app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
|
||||
|
||||
for i, app_name in enumerate(self.app_names):
|
||||
app_module = app_module_paths[i]
|
||||
DjangoHelper.startapp(app_name, apps_base_dir, app_module)
|
||||
UIFormatter.print_success(f"Django app '{app_name}' created successfully!")
|
||||
|
||||
self.add_apps_to_settings()
|
||||
|
||||
def add_apps_to_settings(self) -> None:
|
||||
"""Add all apps to USER_DEFINED_APPS in base.py settings file."""
|
||||
base_settings_path = get_base_settings_path(self.project_root, self.module_name)
|
||||
base_settings_path = CommonUtils.get_base_settings_path(self.project_root, self.module_name)
|
||||
|
||||
if not os.path.exists(base_settings_path):
|
||||
from djinit.utils.exceptions import ConfigError
|
||||
|
||||
raise ConfigError("Could not find base.py settings file")
|
||||
|
||||
content = read_base_settings(self.project_root, self.module_name)
|
||||
content = CommonUtils.read_base_settings(self.project_root, self.module_name)
|
||||
if content is None:
|
||||
from djinit.utils.exceptions import ConfigError
|
||||
|
||||
raise ConfigError("Could not read base.py settings file")
|
||||
|
||||
app_module_paths = calculate_app_module_paths(self.app_names, self.metadata)
|
||||
nested = bool(self.metadata.get("nested_apps"))
|
||||
nested_dir = self.metadata.get("nested_dir")
|
||||
app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
|
||||
full_config_paths = [CommonUtils.get_full_app_config_path(name, nested, nested_dir) for name in self.app_names]
|
||||
|
||||
existing_apps = extract_existing_apps(content)
|
||||
apps_to_add = [app for app in app_module_paths if app not in existing_apps]
|
||||
existing_apps = CommonUtils.extract_existing_apps(content)
|
||||
|
||||
# Check against both module path and full config path to avoid duplicates
|
||||
apps_to_add = []
|
||||
for i, full_path in enumerate(full_config_paths):
|
||||
mod_path = app_module_paths[i]
|
||||
if full_path not in existing_apps and mod_path not in existing_apps:
|
||||
apps_to_add.append(full_path)
|
||||
elif mod_path in existing_apps:
|
||||
# If legacy module path exists, we could upgrade it here,
|
||||
# but ProjectManager is typically initialized on creation.
|
||||
# For now, let's just not add a duplicate.
|
||||
# Actually, let's upgrade it if we can
|
||||
pass
|
||||
|
||||
# If we found apps to add, insert them
|
||||
if not apps_to_add:
|
||||
# Check if we need to upgrade any legacy paths
|
||||
upgraded = False
|
||||
for i, full_path in enumerate(full_config_paths):
|
||||
mod_path = app_module_paths[i]
|
||||
if mod_path in existing_apps and full_path not in existing_apps:
|
||||
content = CommonUtils.replace_app_in_user_defined_apps(content, mod_path, full_path)
|
||||
upgraded = True
|
||||
|
||||
if upgraded:
|
||||
with open(base_settings_path, "w") as f:
|
||||
f.write(content)
|
||||
UIFormatter.print_success("Upgraded existing apps to full AppConfig paths")
|
||||
return
|
||||
|
||||
UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS")
|
||||
return
|
||||
|
||||
updated_content = insert_apps_into_user_defined_apps(content, apps_to_add)
|
||||
updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, apps_to_add)
|
||||
if not updated_content:
|
||||
from djinit.utils.exceptions import ConfigError
|
||||
|
||||
@@ -89,8 +122,10 @@ class ProjectManager:
|
||||
UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}")
|
||||
|
||||
def validate_project_structure(self) -> None:
|
||||
"""Validate project structure - only checks essential configuration files."""
|
||||
join = lambda *args: os.path.join(self.project_root, *args)
|
||||
|
||||
# Core Django project files
|
||||
required_files = [
|
||||
join("manage.py"),
|
||||
join(self.module_name, "__init__.py"),
|
||||
@@ -125,18 +160,19 @@ class ProjectManager:
|
||||
required_files.extend(unified_files)
|
||||
|
||||
elif not self.metadata.get("predefined_structure") and not self.metadata.get("single_structure"):
|
||||
# Standard structure - validate all standard Django app files
|
||||
for app_name in self.app_names:
|
||||
app_path = lambda f, app=app_name: os.path.join(apps_base_dir, app, f)
|
||||
app_files = [
|
||||
app_path("__init__.py"),
|
||||
app_path("apps.py"),
|
||||
app_path("admin.py"),
|
||||
app_path("models.py"),
|
||||
app_path("views.py"),
|
||||
app_path("serializers.py"),
|
||||
app_path("routes.py"),
|
||||
app_path("urls.py"),
|
||||
app_path("tests.py"),
|
||||
app_path("migrations"),
|
||||
app_path("admin.py"),
|
||||
]
|
||||
required_files.extend(app_files)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"""
|
||||
Main CLI orchestrator for djinit.
|
||||
Coordinates between different managers to create a complete Django project.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
from djinit.services.files import FileManager
|
||||
from djinit.services.project import ProjectManager
|
||||
from djinit.creators.files import FileCreator
|
||||
from djinit.creators.project import ProjectCreator
|
||||
from djinit.ui.console import UIFormatter
|
||||
|
||||
|
||||
class Cli:
|
||||
class SetupCreator:
|
||||
"""
|
||||
Orchestrator for creating a complete Django project.
|
||||
Combines ProjectCreator and FileCreator to set up the environment.
|
||||
"""
|
||||
|
||||
def __init__(self, project_dir: str, project_name: str, primary_app: str, app_names: list, metadata: dict):
|
||||
self.project_dir = project_dir
|
||||
self.project_name = project_name
|
||||
@@ -23,8 +23,8 @@ class Cli:
|
||||
else:
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
|
||||
self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata)
|
||||
self.file_manager = FileManager(self.project_root, project_name, app_names, metadata)
|
||||
self.project_creator = ProjectCreator(project_dir, project_name, app_names, metadata)
|
||||
self.file_creator = FileCreator(self.project_root, project_name, app_names, metadata)
|
||||
|
||||
def _normalize_metadata(self) -> None:
|
||||
"""Normalize metadata and app names based on structure type."""
|
||||
@@ -34,7 +34,7 @@ class Cli:
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
if not self.app_names:
|
||||
self.app_names = ["users", "core"]
|
||||
self.project_manager.app_names = self.app_names
|
||||
self.project_creator.app_names = self.app_names
|
||||
|
||||
elif self.metadata.get("unified_structure"):
|
||||
self.metadata.setdefault("project_module_name", "core")
|
||||
@@ -42,44 +42,44 @@ class Cli:
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
if not self.app_names:
|
||||
self.app_names = []
|
||||
self.project_manager.app_names = self.app_names
|
||||
self.project_creator.app_names = self.app_names
|
||||
|
||||
elif self.metadata.get("single_structure"):
|
||||
self.metadata.setdefault("nested_apps", False)
|
||||
if not self.app_names:
|
||||
self.app_names = []
|
||||
self.project_manager.app_names = self.app_names
|
||||
self.project_creator.app_names = self.app_names
|
||||
|
||||
def run_setup(self) -> bool:
|
||||
def create(self) -> bool:
|
||||
"""Run the full setup process."""
|
||||
self._normalize_metadata()
|
||||
|
||||
steps: List[Tuple[str, Callable[[], None]]] = []
|
||||
|
||||
steps.append(("Creating Django project", self.project_manager.create_project))
|
||||
steps.append(("Creating Django project", self.project_creator.create_project))
|
||||
|
||||
if self.metadata.get("unified_structure"):
|
||||
steps.append(("Creating unified structure", self.file_manager.create_unified_structure))
|
||||
steps.append(("Creating unified structure", self.file_creator.create_unified_structure))
|
||||
elif self.metadata.get("single_structure"):
|
||||
steps.append(("Creating single folder structure", self.file_manager.create_single_structure))
|
||||
steps.append(("Creating single folder structure", self.file_creator.create_single_structure))
|
||||
elif self.metadata.get("predefined_structure"):
|
||||
steps.append(("Creating predefined structure", self.file_manager.create_predefined_structure))
|
||||
steps.append(("Adding apps to settings", self.project_manager.add_apps_to_settings))
|
||||
steps.append(("Creating predefined structure", self.file_creator.create_predefined_structure))
|
||||
steps.append(("Adding apps to settings", self.project_creator.add_apps_to_settings))
|
||||
else:
|
||||
steps.append(("Creating Django apps", self.project_manager.create_apps))
|
||||
steps.append(("Creating project URLs", self.file_manager.create_project_urls))
|
||||
steps.append(("Creating Django apps", self.project_creator.create_apps))
|
||||
steps.append(("Creating project URLs", self.file_creator.create_project_urls))
|
||||
|
||||
steps.extend(
|
||||
[
|
||||
("Validating project structure", self.project_manager.validate_project_structure),
|
||||
("Validating project structure", self.project_creator.validate_project_structure),
|
||||
("Creating utility files", self._create_utility_files),
|
||||
("Creating Procfile", self.file_manager.create_procfile),
|
||||
("Creating Justfile", self.file_manager.create_justfile),
|
||||
("Creating runtime.txt", self.file_manager.create_runtime_txt),
|
||||
("Creating Procfile", self.file_creator.create_procfile),
|
||||
("Creating Justfile", self.file_creator.create_justfile),
|
||||
("Creating runtime.txt", self.file_creator.create_runtime_txt),
|
||||
("Creating CI/CD pipelines", self._create_cicd_pipelines),
|
||||
]
|
||||
)
|
||||
|
||||
total_steps = len(steps)
|
||||
total_steps = len(steps)
|
||||
success = True
|
||||
|
||||
@@ -99,12 +99,12 @@ class Cli:
|
||||
|
||||
def _create_utility_files(self) -> None:
|
||||
utility_steps = [
|
||||
self.file_manager.create_gitignore,
|
||||
self.file_manager.create_requirements,
|
||||
self.file_manager.create_readme,
|
||||
self.file_manager.create_env_file,
|
||||
self.file_manager.create_djinit_config,
|
||||
lambda: self.file_manager.create_pyproject_toml(self.metadata),
|
||||
self.file_creator.create_gitignore,
|
||||
self.file_creator.create_requirements,
|
||||
self.file_creator.create_readme,
|
||||
self.file_creator.create_env_file,
|
||||
self.file_creator.create_djinit_config,
|
||||
lambda: self.file_creator.create_pyproject(self.metadata),
|
||||
]
|
||||
|
||||
for step_func in utility_steps:
|
||||
@@ -114,7 +114,7 @@ class Cli:
|
||||
|
||||
def _create_cicd_pipelines(self) -> None:
|
||||
if self.metadata.get("use_github_actions", True):
|
||||
self.file_manager.create_github_actions()
|
||||
self.file_creator.create_github_actions()
|
||||
|
||||
if self.metadata.get("use_gitlab_ci", True):
|
||||
self.file_manager.create_gitlab_ci()
|
||||
self.file_creator.create_gitlab_ci()
|
||||
@@ -7,10 +7,11 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, Template
|
||||
from djinit.core.base import BaseService
|
||||
from djinit.core.parser import InFileLogicParser
|
||||
|
||||
|
||||
class TemplateEngine:
|
||||
class TemplateEngine(BaseService):
|
||||
"""Template engine for rendering file templates."""
|
||||
|
||||
def __init__(self, template_dir: str = None):
|
||||
@@ -20,29 +21,35 @@ class TemplateEngine:
|
||||
template_dir: Path to templates directory. If None, uses default.
|
||||
"""
|
||||
if template_dir is None:
|
||||
# djinit/services/templates.py -> djinit/services -> djinit -> templates
|
||||
# djinit/templater/__init__.py -> djinit/templater -> djinit -> templates
|
||||
template_dir = Path(__file__).resolve().parent.parent / "templates"
|
||||
|
||||
super().__init__()
|
||||
|
||||
if not template_dir.exists():
|
||||
raise FileNotFoundError(f"Template directory not found: {template_dir}")
|
||||
|
||||
self.template_dir = str(template_dir)
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader(template_dir), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True
|
||||
)
|
||||
self.parser = InFileLogicParser()
|
||||
|
||||
def render_template(self, template_name: str, context: Dict[str, Any]) -> str:
|
||||
"""Render a template with the given context.
|
||||
|
||||
Args:
|
||||
template_name: Name of the template file (e.g., 'gitignore.j2')
|
||||
template_name: Name of the template file (e.g., 'gitignore.py-tpl')
|
||||
context: Dictionary of variables to pass to the template
|
||||
|
||||
Returns:
|
||||
Rendered template content as string
|
||||
"""
|
||||
template = self.env.get_template(template_name)
|
||||
return template.render(**context)
|
||||
template_path = os.path.join(self.template_dir, template_name)
|
||||
if not os.path.exists(template_path):
|
||||
raise FileNotFoundError(f"Template not found: {template_path}")
|
||||
|
||||
with open(template_path) as f:
|
||||
template_text = f.read()
|
||||
|
||||
return self.parser.render(template_text, context)
|
||||
|
||||
def render_string(self, template_string: str, context: Dict[str, Any]) -> str:
|
||||
"""Render a template string with the given context.
|
||||
@@ -54,24 +61,26 @@ class TemplateEngine:
|
||||
Returns:
|
||||
Rendered template content as string
|
||||
"""
|
||||
template = Template(template_string, trim_blocks=True, lstrip_blocks=True)
|
||||
return template.render(**context)
|
||||
return self.parser.render(template_string, context)
|
||||
|
||||
def get_template_names(self) -> list[str]:
|
||||
"""Get list of available template files."""
|
||||
if not os.path.exists(self.template_dir):
|
||||
return []
|
||||
|
||||
return sorted([file for file in os.listdir(self.template_dir) if file.endswith(".j2")])
|
||||
templates = []
|
||||
for root, _, files in os.walk(self.template_dir):
|
||||
for file in files:
|
||||
if file.endswith("-tpl"):
|
||||
rel_path = os.path.relpath(os.path.join(root, file), self.template_dir)
|
||||
templates.append(rel_path)
|
||||
return sorted(templates)
|
||||
|
||||
def template_exists(self, template_name: str) -> bool:
|
||||
"""Check if a template file exists.
|
||||
|
||||
Args:
|
||||
template_name: Name of the template file
|
||||
|
||||
Returns:
|
||||
True if template exists, False otherwise
|
||||
"""
|
||||
template_path = os.path.join(self.template_dir, template_name)
|
||||
return os.path.exists(template_path)
|
||||
@@ -0,0 +1,75 @@
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from djinit.templater import template_engine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TemplateFile:
|
||||
"""A template file to be rendered.
|
||||
|
||||
The template file extension should end with '-tpl'.
|
||||
"""
|
||||
|
||||
path: Path
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
template_name: str | None = None # Optional override, otherwise derived from path name + -tpl
|
||||
|
||||
|
||||
def create_file(
|
||||
template_file: TemplateFile,
|
||||
template_name: str | None = None,
|
||||
) -> None:
|
||||
"""Create file based on a template.
|
||||
|
||||
Args:
|
||||
template_file: The TemplateFile object containing path and context.
|
||||
template_name: Optional explicit template name. If not provided,
|
||||
it looks for template_file.template_name,
|
||||
or defaults to template_file.path.name + "-tpl".
|
||||
"""
|
||||
path = template_file.path
|
||||
|
||||
logger.debug(f"Create file, {path}, if it doesn't exist")
|
||||
|
||||
# Ensure parent directory exists
|
||||
if not path.parent.exists():
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Create __init__.py in parents if needed?
|
||||
# CommonUtils.create_directory_with_init handles this usually,
|
||||
# but here we are being more granular.
|
||||
# For now, let's assume specific directories are created or we blindly create parents.
|
||||
# But wait, django-new logic creates parents.
|
||||
|
||||
if path.exists():
|
||||
# logic to overwrite or skip? django-new skips if exists usually,
|
||||
# or maybe we want to force?
|
||||
# For now, let's follow django-new:
|
||||
logger.debug(f"Do not create template file, {path}, because it already exists")
|
||||
# However, djinit usually overwrites or at least proceeds.
|
||||
# Let's keep it safe.
|
||||
pass
|
||||
# Actually, let's overwrite if needed or warn.
|
||||
# But CommonUtils.create_file_from_template overwrites.
|
||||
# Let's stick to djinit behavior of overwriting/creating content.
|
||||
|
||||
# Check explicitly passed template_name -> template_file.template_name -> path.name + "-tpl"
|
||||
tpl_name = template_name or template_file.template_name or (path.name + "-tpl")
|
||||
|
||||
try:
|
||||
content = template_engine.render_template(tpl_name, template_file.context)
|
||||
|
||||
with open(path, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
logger.debug(f"Created template file, {path}")
|
||||
# We might want to print success via UIFormatter, but maybe keep this low level.
|
||||
|
||||
except FileNotFoundError:
|
||||
# Fallback or error
|
||||
logger.error(f"Template {tpl_name} not found.")
|
||||
raise
|
||||
File renamed without changes.
+3
-3
@@ -24,7 +24,7 @@ If you have a `User` model in `models/user.py`, create:
|
||||
### 1. `api/user/serializers.py`
|
||||
```python
|
||||
from rest_framework import serializers
|
||||
from {{ module_name }}.models.user import User
|
||||
from [[ module_name ]].models.user import User
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
@@ -35,7 +35,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
### 2. `api/user/views.py`
|
||||
```python
|
||||
from rest_framework import viewsets
|
||||
from {{ module_name }}.models.user import User
|
||||
from [[ module_name ]].models.user import User
|
||||
from .serializers import UserSerializer
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
@@ -62,7 +62,7 @@ urlpatterns = [
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('user/', include('{{ module_name }}.api.user.urls')),
|
||||
path('user/', include('[[ module_name ]].api.user.urls')),
|
||||
# Add more API endpoints here
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
||||
class [[ app_config_name ]]Config(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
# @IF app_module
|
||||
name = '[[ app_module ]]'
|
||||
# @ENDIF
|
||||
# @IF not app_module
|
||||
name = '[[ app_name ]]'
|
||||
# @ENDIF
|
||||
File renamed without changes.
+1
-1
@@ -69,7 +69,7 @@ Create or update `admin/__init__.py`:
|
||||
|
||||
```python
|
||||
from django.contrib import admin
|
||||
from {{ module_name }}.models import User
|
||||
from [[ module_name ]].models import User
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
@@ -0,0 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
# Create your serializers here.
|
||||
File renamed without changes.
@@ -1,11 +1,7 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = '{{ app_name }}'
|
||||
|
||||
urlpatterns = [
|
||||
# Add your URL patterns here
|
||||
# path('', views.index, name='index'),
|
||||
]
|
||||
@@ -1,5 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.shortcuts import render
|
||||
from rest_framework import viewsets
|
||||
|
||||
# Create your views here.
|
||||
@@ -7,5 +7,5 @@ from django.core.asgi import get_asgi_application
|
||||
env = environ.Env()
|
||||
environ.Env.read_env()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="core.settings.production"))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
|
||||
application = get_asgi_application()
|
||||
+58
-9
@@ -26,11 +26,15 @@ THIRD_PARTY_APPS = [
|
||||
"rest_framework_simplejwt.token_blacklist",
|
||||
"corsheaders",
|
||||
"drf_spectacular",
|
||||
"django_tailwind_cli", # @IF use_tailwind
|
||||
"django_htmx", # @IF use_htmx
|
||||
]
|
||||
|
||||
USER_DEFINED_APPS = [
|
||||
{% for app in app_names %} "{{ app }}",
|
||||
{% endfor %}]
|
||||
# @LOOP app in app_names
|
||||
"[[ app ]]",
|
||||
# @ENDLOOP
|
||||
]
|
||||
|
||||
BUILT_IN_APPS = [
|
||||
"django.contrib.admin",
|
||||
@@ -54,9 +58,10 @@ MIDDLEWARE = [
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django_htmx.middleware.HtmxMiddleware", # @IF use_htmx
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "{{ project_name }}.urls"
|
||||
ROOT_URLCONF = "[[ project_name ]].urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
@@ -73,7 +78,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "{{ project_name }}.wsgi.application"
|
||||
WSGI_APPLICATION = "[[ project_name ]].wsgi.application"
|
||||
|
||||
|
||||
# Password validation
|
||||
@@ -104,13 +109,20 @@ USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files and media files
|
||||
STATIC_URL = "static/"
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
|
||||
MEDIA_URL = "media/"
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
@@ -133,8 +145,8 @@ REST_FRAMEWORK = {
|
||||
|
||||
# DRF Spectacular settings
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "{{ project_name }} API",
|
||||
"DESCRIPTION": "API documentation for {{ project_name }}",
|
||||
"TITLE": "[[ project_name ]] API",
|
||||
"DESCRIPTION": "API documentation for [[ project_name ]]",
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
@@ -176,3 +188,40 @@ CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
X_FRAME_OPTIONS = "DENY"
|
||||
|
||||
# @IF use_tailwind
|
||||
# for tailwind
|
||||
STATICFILES_DIRS = [BASE_DIR / "static/"]
|
||||
TAILWIND_CLI_VERSION = "2.7.8"
|
||||
TAILWIND_CLI_USE_DAISY_UI = True
|
||||
TAILWIND_CLI_CONFIG = {
|
||||
"theme": {
|
||||
"extend": {
|
||||
"colors": {
|
||||
"primary": "#DC2626",
|
||||
"secondary": "#EF4444",
|
||||
"accent": "#FCFECC",
|
||||
}
|
||||
}
|
||||
},
|
||||
"daisyui": {
|
||||
"themes": [
|
||||
{
|
||||
"light": {
|
||||
"primary": "#DC2626",
|
||||
"secondary": "#EF4444",
|
||||
"accent": "#FCFECC",
|
||||
"neutral": "#1a1a2e",
|
||||
"base-100": "#ffffff",
|
||||
"base-200": "#f8fafc",
|
||||
"base-300": "#e2e8f0",
|
||||
"info": "#0E4D80",
|
||||
"success": "#16a34a",
|
||||
"warning": "#f59e0b",
|
||||
"error": "#DC2626",
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
# @ENDIF
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
from .base import *
|
||||
|
||||
SECRET_KEY = "{{ secret_key }}"
|
||||
SECRET_KEY = "[[ secret_key ]]"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
+20
-11
@@ -1,36 +1,38 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
# @IF use_database_url
|
||||
import dj_database_url
|
||||
# @ENDIF
|
||||
|
||||
DEBUG = False
|
||||
SECRET_KEY = env('SECRET_KEY')
|
||||
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
|
||||
|
||||
# Database
|
||||
{% if use_database_url %}
|
||||
# @IF use_database_url
|
||||
# Using dj_database_url (recommended for production)
|
||||
DATABASES = {
|
||||
'default': env.db('DATABASE_URL')
|
||||
}
|
||||
{% else %}
|
||||
# @ELSE
|
||||
# Using individual database parameters
|
||||
DATABASES = {
|
||||
'default': {
|
||||
{% if database_type == 'mysql' %}
|
||||
# @IF database_type == 'mysql'
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'PORT': env('DB_PORT', default='3306'),
|
||||
{% else %}
|
||||
# @ELSE
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'PORT': env('DB_PORT', default='5432'),
|
||||
{% endif %}
|
||||
# @ENDIF
|
||||
'NAME': env('DB_NAME'),
|
||||
'USER': env('DB_USER'),
|
||||
'PASSWORD': env('DB_PASSWORD'),
|
||||
'HOST': env('DB_HOST'),
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
# @ENDIF
|
||||
|
||||
# CORS settings for production
|
||||
# Filter out empty strings from FRONTEND_URL
|
||||
@@ -42,13 +44,13 @@ CORS_ALLOW_PRIVATE_NETWORK = False
|
||||
|
||||
# Email settings
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = env('EMAIL_HOST')
|
||||
EMAIL_HOST = env('EMAIL_HOST', default="smtp.gmail.com")
|
||||
EMAIL_PORT = env.int('EMAIL_PORT', default=587)
|
||||
EMAIL_USE_TLS = env.bool('EMAIL_USE_TLS', default=True)
|
||||
EMAIL_HOST_USER = env('EMAIL_HOST_USER')
|
||||
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
|
||||
DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER')
|
||||
SERVER_EMAIL = env('EMAIL_HOST_USER')
|
||||
EMAIL_HOST_USER = env('EMAIL_HOST_USER', default="")
|
||||
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD', default="")
|
||||
DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER', default="")
|
||||
SERVER_EMAIL = env('EMAIL_HOST_USER', default="")
|
||||
|
||||
# Security settings for production
|
||||
SECURE_SSL_REDIRECT = True
|
||||
@@ -63,3 +65,10 @@ CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Static files
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
|
||||
# @IF use_tailwind
|
||||
## Tailwind
|
||||
TAILWIND_CLI_VERSION = "4.1.3" # Pin version
|
||||
TAILWIND_CLI_AUTOMATIC_DOWNLOAD = False # Use pre-installed CLI
|
||||
TAILWIND_CLI_DIST_CSS = "static/css/tailwind.min.css"
|
||||
# @ENDIF
|
||||
@@ -0,0 +1,103 @@
|
||||
# @IF url_type == 'project'
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
# @IF include_api or include_apps
|
||||
from django.urls import include
|
||||
# @ENDIF
|
||||
# @IF include_api
|
||||
from django.conf import settings
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
TokenBlacklistView,
|
||||
)
|
||||
# @ENDIF
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
# @IF include_api
|
||||
# JWT tokens
|
||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
|
||||
# @ENDIF
|
||||
# @IF include_api and comment_out_api_url
|
||||
# path("api/", include("[[ api_module ]].urls")),
|
||||
# @ENDIF
|
||||
# @IF include_api and not comment_out_api_url
|
||||
path("api/", include("[[ api_module ]].urls")),
|
||||
# @ENDIF
|
||||
# @IF include_apps
|
||||
# App URLs
|
||||
# @LOOP app in app_names
|
||||
path("", include("[[ app ]].urls")),
|
||||
# @ENDLOOP
|
||||
# @ENDIF
|
||||
]
|
||||
# @IF include_api
|
||||
|
||||
# Don't show schema in production
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
|
||||
]
|
||||
# @ENDIF
|
||||
# @ENDIF
|
||||
# @IF url_type == 'api_root'
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "[[ app_name ]]"
|
||||
|
||||
urlpatterns = [
|
||||
path("[[ version ]]/", include("[[ api_module ]].[[ version ]].urls")),
|
||||
]
|
||||
# @ENDIF
|
||||
# @IF url_type == 'api_version'
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "[[ app_name ]]"
|
||||
|
||||
urlpatterns = [
|
||||
# @LOOP app in app_list
|
||||
path("[[ app ]]/", include("[[ app_module ]].[[ app ]].urls")),
|
||||
# @ENDLOOP
|
||||
]
|
||||
# @ENDIF
|
||||
# @IF url_type == 'drf_router'
|
||||
from rest_framework.routers import DefaultRouter
|
||||
# @IF has_viewset
|
||||
from [[ app_module ]].views.[[ view_file ]] import [[ viewset_name ]]
|
||||
# @ENDIF
|
||||
# @IF has_app_name
|
||||
|
||||
app_name = "[[ app_name ]]"
|
||||
# @ENDIF
|
||||
|
||||
router = DefaultRouter()
|
||||
# @IF has_viewset
|
||||
router.register(r"[[ router_prefix ]]", [[ viewset_name ]], basename="[[ basename ]]")
|
||||
# @ENDIF
|
||||
|
||||
urlpatterns = [
|
||||
# @IF has_include
|
||||
path("", include(router.urls)),
|
||||
# @ENDIF
|
||||
]
|
||||
# @IF not has_include
|
||||
urlpatterns = router.urls
|
||||
# @ENDIF
|
||||
# @ENDIF
|
||||
# @IF url_type == 'component'
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
# Add your URL patterns here
|
||||
]
|
||||
# @ENDIF
|
||||
@@ -7,6 +7,6 @@ from django.core.wsgi import get_wsgi_application
|
||||
env = environ.Env()
|
||||
environ.Env.read_env()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="core.settings.production"))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
|
||||
|
||||
application = get_wsgi_application()
|
||||
+5
-5
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
python-version: ["3.12", "3.13"]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
@@ -31,10 +31,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python {{ '{{ matrix.python-version }}' }}
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: {{ '{{ matrix.python-version }}' }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
- name: Run Django tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
|
||||
- name: Check migrations
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
# GitLab CI/CD configuration for {{ project_name }}
|
||||
# GitLab CI/CD configuration for [[ project_name ]]
|
||||
|
||||
stages:
|
||||
- test
|
||||
@@ -36,7 +36,7 @@ test:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -52,7 +52,7 @@ test:python310:
|
||||
stage: test
|
||||
image: python:3.10
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -69,7 +69,7 @@ test:python311:
|
||||
stage: test
|
||||
image: python:3.11
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -86,7 +86,7 @@ test:python312:
|
||||
stage: test
|
||||
image: python:3.12
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -103,7 +103,7 @@ test:python313:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -121,7 +121,7 @@ security:
|
||||
stage: security
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
script:
|
||||
- pip install uv
|
||||
@@ -0,0 +1,32 @@
|
||||
# Django settings
|
||||
DJANGO_SETTINGS_MODULE=[[ project_name ]].settings.development
|
||||
SECRET_KEY=your-secret-key
|
||||
ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
|
||||
|
||||
# @IF use_database_url
|
||||
# Database URL (recommended for production)
|
||||
# @IF database_type == 'mysql'
|
||||
DATABASE_URL=mysql://user:password@localhost:3306/[[ project_name ]]
|
||||
# @ELSE
|
||||
DATABASE_URL=postgres://user:password@localhost:5432/[[ project_name ]]
|
||||
# @ENDIF
|
||||
# @ELSE
|
||||
# Database (individual parameters)
|
||||
DB_NAME=
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
# @ENDIF
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=https://frontend.your-domain.com,www.frontend.your-domain.com
|
||||
|
||||
# Email settings
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=
|
||||
SERVER_EMAIL=
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
# Justfile for {{ project_name }}
|
||||
# Justfile for [[ project_name ]]
|
||||
# Task runner for Django development
|
||||
# Install just: https://github.com/casey/just
|
||||
|
||||
@@ -12,7 +12,7 @@ dev:
|
||||
|
||||
# Run development server with custom host and port
|
||||
dev-server HOST="0.0.0.0" PORT="8000":
|
||||
uv run python manage.py runserver {{ '{{' }}HOST{{ '}}' }}:{{ '{{' }}PORT{{ '}}' }}
|
||||
uv run python manage.py runserver {{ HOST }}:{{ PORT }}
|
||||
|
||||
# Create migrations for all apps
|
||||
makemigrations:
|
||||
@@ -20,7 +20,7 @@ makemigrations:
|
||||
|
||||
# Create migrations for specific app
|
||||
makemigrations-app APP:
|
||||
uv run python manage.py makemigrations {{ '{{' }}APP{{ '}}' }}
|
||||
uv run python manage.py makemigrations {{ APP }}
|
||||
|
||||
# Run all migrations
|
||||
migrate:
|
||||
@@ -82,7 +82,17 @@ showmigrations:
|
||||
|
||||
# Sync dependencies (uv equivalent of pip freeze)
|
||||
requirements:
|
||||
uv pip compile
|
||||
#!/usr/bin/env -S uv run python
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
pyproject = Path("pyproject.toml")
|
||||
if not pyproject.exists():
|
||||
print("pyproject.toml not found")
|
||||
raise SystemExit(1)
|
||||
data = tomllib.loads(pyproject.read_text())
|
||||
deps = data.get("project", {}).get("dependencies", [])
|
||||
Path("requirements.txt").write_text("\n".join(deps) + "\n")
|
||||
print("Generated requirements.txt from pyproject.toml")
|
||||
|
||||
# Run all linting and checks
|
||||
ci:
|
||||
@@ -108,15 +118,15 @@ dbshell:
|
||||
|
||||
# Start production server with gunicorn
|
||||
server:
|
||||
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000
|
||||
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000
|
||||
|
||||
# Start production server with gunicorn workers
|
||||
server-prod WORKERS="4":
|
||||
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --workers {{ '{{' }}WORKERS{{ '}}' }}
|
||||
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000 --workers {{ WORKERS }}
|
||||
|
||||
# Start production server with gunicorn and reload on file changes
|
||||
server-watch:
|
||||
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --reload
|
||||
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000 --reload
|
||||
|
||||
# Clean Python cache files
|
||||
clean:
|
||||
@@ -208,12 +218,12 @@ setup-complete:
|
||||
|
||||
# Install a new package
|
||||
install PACKAGE:
|
||||
uv add {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
uv add {{ PACKAGE }}
|
||||
|
||||
# Install a dev package
|
||||
install-dev PACKAGE:
|
||||
uv add --dev {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
uv add --dev {{ PACKAGE }}
|
||||
|
||||
# Remove a package
|
||||
remove PACKAGE:
|
||||
uv remove {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
uv remove {{ PACKAGE }}
|
||||
@@ -11,7 +11,7 @@ import environ
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "[[ project_name ]].settings.production"))
|
||||
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
@@ -0,0 +1,20 @@
|
||||
# Procfile for [[ project_name ]]
|
||||
# Used for deploying to PaaS platforms like Heroku, Railway, Render, etc.
|
||||
|
||||
# Web server process (using Gunicorn)
|
||||
# Adjust workers based on your needs: (2 x CPU cores) + 1
|
||||
web: gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info
|
||||
|
||||
# Alternative web server configurations (uncomment to use):
|
||||
# web: gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120
|
||||
# web: gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120
|
||||
|
||||
# Release task (runs migrations before deployment)
|
||||
release: python manage.py migrate --noinput && python manage.py collectstatic --noinput
|
||||
|
||||
# Uncomment below to run scheduled tasks (if using Celery)
|
||||
# worker: celery -A [[ module_name ]] worker --loglevel=info
|
||||
# beat: celery -A [[ module_name ]] beat --loglevel=info
|
||||
|
||||
# Development server (for local testing with Foreman/Overmind)
|
||||
# web: python manage.py runserver 0.0.0.0:$PORT
|
||||
+11
-4
@@ -1,5 +1,5 @@
|
||||
[project]
|
||||
name = "{{ package_name }}"
|
||||
name = "[[ package_name ]]"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
readme = "README.md"
|
||||
@@ -14,14 +14,21 @@ dependencies = [
|
||||
"drf-spectacular",
|
||||
"django-cors-headers",
|
||||
"whitenoise",
|
||||
# @IF database_type == 'mysql'
|
||||
"mysqlclient",
|
||||
"psycopg2-binary",
|
||||
# @ENDIF
|
||||
# @IF database_type == 'postgresql'
|
||||
"psycopg[binary]",
|
||||
# @ENDIF
|
||||
# @IF use_database_url
|
||||
"dj-database-url",
|
||||
# @ENDIF
|
||||
"gunicorn",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py38"
|
||||
target-version = "py313"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
@@ -39,4 +46,4 @@ ignore = [
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["{{ project_name }}"]
|
||||
known-first-party = ["[[ module_name ]]"]
|
||||
@@ -1,4 +1,4 @@
|
||||
# {{ project_name }}
|
||||
# [[ project_name ]]
|
||||
|
||||
A Django project folder structure is generated with [djinitx/djinit/dj](https://github.com/S4NKALP/djinit).
|
||||
|
||||
@@ -71,9 +71,9 @@ Run `just` to see all available commands, including:
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
{% if predefined_structure %}
|
||||
{{ project_name }}/
|
||||
├── {{ module_name }}/
|
||||
# @IF predefined_structure
|
||||
[[ project_name ]]/
|
||||
├── [[ module_name ]]/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
@@ -118,8 +118,8 @@ Run `just` to see all available commands, including:
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
{% elif unified_structure %}
|
||||
{{ project_name }}/
|
||||
# @ELSEIF unified_structure
|
||||
[[ project_name ]]/
|
||||
├── apps/
|
||||
│ ├── admin/
|
||||
│ │ └── __init__.py
|
||||
@@ -158,9 +158,9 @@ Run `just` to see all available commands, including:
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
{% else %}
|
||||
{{ project_name }}/
|
||||
├── {{ module_name }}/
|
||||
# @ELSE
|
||||
[[ project_name ]]/
|
||||
├── [[ module_name ]]/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
@@ -191,7 +191,7 @@ For example, if you have a `User` model in `models/user.py`, create:
|
||||
- `api/user/urls.py` - URL patterns for User endpoints
|
||||
|
||||
See the README files in `api/` and `models/` directories for more details.
|
||||
{% endif %}
|
||||
# @ENDIF
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
+6
-4
@@ -10,10 +10,12 @@ djangorestframework_simplejwt
|
||||
drf-spectacular
|
||||
django-cors-headers
|
||||
whitenoise
|
||||
{% if database_type == 'mysql' %}
|
||||
django-tailwind-cli # @IF use_tailwind
|
||||
django-htmx # @IF use_htmx
|
||||
# @IF database_type == 'mysql'
|
||||
mysqlclient
|
||||
{% else %}
|
||||
# @ELSE
|
||||
psycopg[binary]
|
||||
{% endif %}
|
||||
# @ENDIF
|
||||
gunicorn
|
||||
{% if use_database_url %}dj-database-url{% endif %}
|
||||
dj-database-url # @IF use_database_url
|
||||
@@ -0,0 +1 @@
|
||||
python-[[ python_version ]]
|
||||
File renamed without changes.
File renamed without changes.
@@ -11,7 +11,7 @@ import questionary
|
||||
|
||||
from djinit.core.types import ProjectMetadata
|
||||
from djinit.ui.console import UIColors, UIFormatter, console
|
||||
from djinit.utils.common import get_package_name
|
||||
from djinit.utils.common import CommonUtils
|
||||
from djinit.utils.validators import validate_app_name, validate_project_name
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ class StructureOptions(TypedDict):
|
||||
single_module_name: str | None
|
||||
database_type: str
|
||||
use_database_url: bool
|
||||
use_tailwind: bool
|
||||
use_htmx: bool
|
||||
use_github: bool
|
||||
use_gitlab: bool
|
||||
|
||||
@@ -171,6 +173,12 @@ class InputCollector:
|
||||
|
||||
return self._get_selection("Choose database:", choices, default="postgresql")
|
||||
|
||||
def get_tailwind_choice(self) -> bool:
|
||||
return UIFormatter.confirm("Include Tailwind CSS? (via django-tailwind-cli)", default=True)
|
||||
|
||||
def get_htmx_choice(self) -> bool:
|
||||
return UIFormatter.confirm("Include HTMX? (via django-htmx)", default=True)
|
||||
|
||||
def _get_structure_metadata(self, options: StructureOptions) -> Tuple[str, str, list[str], dict]:
|
||||
"""Helper method to generate metadata dictionary."""
|
||||
project_dir = options["project_dir"]
|
||||
@@ -178,7 +186,7 @@ class InputCollector:
|
||||
app_names: list[str] = []
|
||||
|
||||
# Default package_name to "backend" if project_dir is "." or empty
|
||||
package_name = get_package_name(project_dir)
|
||||
package_name = CommonUtils.get_package_name(project_dir)
|
||||
|
||||
project_module_name = "config" if options["predefined"] else "core" if options["unified"] else None
|
||||
|
||||
@@ -193,6 +201,8 @@ class InputCollector:
|
||||
nested_dir="apps" if not options["single"] else None,
|
||||
use_database_url=options["use_database_url"],
|
||||
database_type=options["database_type"],
|
||||
use_tailwind=options.get("use_tailwind", False),
|
||||
use_htmx=options.get("use_htmx", False),
|
||||
predefined_structure=options["predefined"],
|
||||
unified_structure=options["unified"],
|
||||
single_structure=options["single"],
|
||||
@@ -250,6 +260,10 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
database_type = collector.get_database_type_choice()
|
||||
use_database_url = collector.get_database_config_choice()
|
||||
|
||||
# Step 3: Tailwind and HTMX
|
||||
use_tailwind = collector.get_tailwind_choice()
|
||||
use_htmx = collector.get_htmx_choice()
|
||||
|
||||
# Step 3: Django Apps (Standard only)
|
||||
nested = False
|
||||
nested_dir = None
|
||||
@@ -264,7 +278,7 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
|
||||
# Generate Metadata
|
||||
if use_standard:
|
||||
package_name = get_package_name(project_dir)
|
||||
package_name = CommonUtils.get_package_name(project_dir)
|
||||
metadata = ProjectMetadata(
|
||||
package_name=package_name,
|
||||
use_github_actions=use_github,
|
||||
@@ -273,6 +287,8 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
nested_dir=nested_dir,
|
||||
use_database_url=use_database_url,
|
||||
database_type=database_type,
|
||||
use_tailwind=use_tailwind,
|
||||
use_htmx=use_htmx,
|
||||
)
|
||||
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
|
||||
else:
|
||||
@@ -284,6 +300,8 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
single_module_name=single_module_name,
|
||||
database_type=database_type,
|
||||
use_database_url=use_database_url,
|
||||
use_tailwind=use_tailwind,
|
||||
use_htmx=use_htmx,
|
||||
use_github=use_github,
|
||||
use_gitlab=use_gitlab,
|
||||
)
|
||||
@@ -316,6 +334,12 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata
|
||||
db_type = metadata.get("database_type", "postgresql").capitalize()
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Database Type:[/{UIColors.HIGHLIGHT}] {db_type}")
|
||||
|
||||
use_tailwind = "Yes" if metadata.get("use_tailwind", False) else "No"
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Tailwind CSS:[/{UIColors.HIGHLIGHT}] {use_tailwind}")
|
||||
|
||||
use_htmx = "Yes" if metadata.get("use_htmx", False) else "No"
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]HTMX:[/{UIColors.HIGHLIGHT}] {use_htmx}")
|
||||
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print()
|
||||
File renamed without changes.
@@ -0,0 +1,352 @@
|
||||
"""
|
||||
Shared utility functions.
|
||||
Contains common operations used across multiple managers.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
from djinit.core.base import BaseUtils
|
||||
from djinit.templater import template_engine
|
||||
from djinit.ui.console import UIFormatter
|
||||
|
||||
|
||||
class CommonUtils(BaseUtils):
|
||||
@staticmethod
|
||||
def format_file(filename: str) -> None:
|
||||
"""Format Python file using Ruff formatter."""
|
||||
subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True)
|
||||
|
||||
@staticmethod
|
||||
def create_file_with_content(
|
||||
filename: str, content: str, success_message: str, should_format: bool = False
|
||||
) -> None:
|
||||
from djinit.utils.exceptions import FileError
|
||||
|
||||
try:
|
||||
with open(filename, "w") as file:
|
||||
file.write(content)
|
||||
except OSError as e:
|
||||
raise FileError(f"Failed to write file: {filename}", details=str(e)) from e
|
||||
|
||||
if should_format:
|
||||
CommonUtils.format_file(filename)
|
||||
|
||||
UIFormatter.print_success(success_message)
|
||||
|
||||
@staticmethod
|
||||
def create_file_from_template(
|
||||
file_path: str,
|
||||
template_path: str,
|
||||
context: dict,
|
||||
success_message: str,
|
||||
should_format: bool = True,
|
||||
) -> None:
|
||||
try:
|
||||
content = template_engine.render_template(template_path, context)
|
||||
except Exception as e:
|
||||
from djinit.utils.exceptions import TemplateError
|
||||
|
||||
raise TemplateError(f"Failed to render template: {template_path}", details=str(e)) from e
|
||||
|
||||
CommonUtils.create_file_with_content(file_path, content, success_message, should_format=should_format)
|
||||
|
||||
@staticmethod
|
||||
def create_init_file(directory: str, success_message: str) -> None:
|
||||
init_path = os.path.join(directory, "__init__.py")
|
||||
CommonUtils.create_file_from_template(
|
||||
init_path, "project/init.py-tpl", {}, success_message, should_format=False
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_directory_with_init(dir_path: str, message: str) -> None:
|
||||
"""Create directory and __init__.py file."""
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
CommonUtils.create_init_file(dir_path, message)
|
||||
|
||||
@staticmethod
|
||||
def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None:
|
||||
"""Create multiple files from templates.
|
||||
|
||||
Args:
|
||||
base_dir: Base directory where files will be created
|
||||
files: List of tuples (filename, template_path, context)
|
||||
prefix: Optional prefix for success message
|
||||
should_format: Whether to format the created files
|
||||
"""
|
||||
for file_spec in files:
|
||||
if len(file_spec) == 3:
|
||||
filename, template_path, context = file_spec
|
||||
else:
|
||||
# Support (filename, template_path) format with empty context
|
||||
filename, template_path = file_spec
|
||||
context = {}
|
||||
filepath = os.path.join(base_dir, filename)
|
||||
message = f"Created {prefix}{filename}" if prefix else f"Created {filename}"
|
||||
CommonUtils.create_file_from_template(
|
||||
filepath, template_path, context, message, should_format=should_format
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_package_name(project_dir: str) -> str:
|
||||
"""Get package name, defaulting to 'backend' if project_dir is '.' or empty."""
|
||||
return "backend" if project_dir == "." or not project_dir else project_dir
|
||||
|
||||
@staticmethod
|
||||
@contextmanager
|
||||
def change_cwd(path: str):
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(path)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
@staticmethod
|
||||
def _parse_app_entry_from_line(line: str) -> str | None:
|
||||
"""Extract app name from a line in USER_DEFINED_APPS section."""
|
||||
line_stripped = line.strip().strip(",").strip()
|
||||
for quote in ('"', "'"):
|
||||
if line_stripped.startswith(quote) and line_stripped.endswith(quote):
|
||||
return line_stripped.strip(quote)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _is_user_apps_section_start(line: str) -> bool:
|
||||
"""Check if line starts USER_DEFINED_APPS section."""
|
||||
return "USER_DEFINED_APPS" in line and "=" in line
|
||||
|
||||
@staticmethod
|
||||
def _iterate_user_apps_lines(content: str):
|
||||
"""Generator that yields lines within USER_DEFINED_APPS section."""
|
||||
lines = content.split("\n")
|
||||
in_user_apps = False
|
||||
|
||||
for line in lines:
|
||||
if CommonUtils._is_user_apps_section_start(line):
|
||||
in_user_apps = True
|
||||
elif in_user_apps and line.strip() == "]":
|
||||
break
|
||||
elif in_user_apps:
|
||||
yield line
|
||||
|
||||
@staticmethod
|
||||
def extract_existing_apps(content: str) -> set:
|
||||
existing_apps = set()
|
||||
for line in CommonUtils._iterate_user_apps_lines(content):
|
||||
app_name = CommonUtils._parse_app_entry_from_line(line)
|
||||
if app_name:
|
||||
existing_apps.add(app_name)
|
||||
return existing_apps
|
||||
|
||||
@staticmethod
|
||||
def format_app_entries(apps: list) -> list:
|
||||
return [f' "{app}",' for app in apps]
|
||||
|
||||
@staticmethod
|
||||
def is_app_section_start(line: str) -> bool:
|
||||
return "=" in line and any(
|
||||
keyword in line for keyword in ("BUILT_IN_APPS", "THIRD_PARTY_APPS", "INSTALLED_APPS")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool:
|
||||
for next_idx in range(current_idx + 1, min(current_idx + 3, len(lines))):
|
||||
next_line = lines[next_idx].strip()
|
||||
if next_line and "BUILT_IN_APPS" in next_line:
|
||||
return True
|
||||
return True # Default to True if we're inside USER_DEFINED_APPS
|
||||
|
||||
@staticmethod
|
||||
def split_bracket_line(line: str, apps_to_add: list) -> list:
|
||||
bracket_pos = line.rfind("]")
|
||||
before_bracket = line[:bracket_pos].rstrip() if bracket_pos > 0 else line.rstrip()
|
||||
after_bracket = line[bracket_pos:] if bracket_pos > 0 else "]"
|
||||
|
||||
result = [before_bracket]
|
||||
result.extend(CommonUtils.format_app_entries(apps_to_add))
|
||||
result.append(after_bracket)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str:
|
||||
lines = content.split("\n")
|
||||
new_lines = []
|
||||
in_user_apps = False
|
||||
apps_added = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if CommonUtils._is_user_apps_section_start(line) and not apps_added:
|
||||
in_user_apps = True
|
||||
|
||||
if "]" in line:
|
||||
new_lines.extend(CommonUtils.split_bracket_line(line, apps_to_add))
|
||||
apps_added = True
|
||||
in_user_apps = False
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
elif in_user_apps and line.strip() == "]" and not apps_added:
|
||||
if CommonUtils.is_user_apps_closing_bracket(lines, i):
|
||||
new_lines.extend(CommonUtils.format_app_entries(apps_to_add))
|
||||
new_lines.append(line)
|
||||
apps_added = True
|
||||
in_user_apps = False
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
elif in_user_apps and CommonUtils.is_app_section_start(line) and not apps_added:
|
||||
new_lines.extend(CommonUtils.format_app_entries(apps_to_add))
|
||||
new_lines.append("]")
|
||||
new_lines.append(line)
|
||||
apps_added = True
|
||||
in_user_apps = False
|
||||
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if not apps_added:
|
||||
UIFormatter.print_error("Could not find or update USER_DEFINED_APPS section in base.py")
|
||||
return None
|
||||
|
||||
return "\n".join(new_lines)
|
||||
|
||||
@staticmethod
|
||||
def replace_app_in_user_defined_apps(content: str, old_app: str, new_app: str) -> str | None:
|
||||
"""Replace an app entry in USER_DEFINED_APPS with a new one."""
|
||||
lines = content.split("\n")
|
||||
new_lines = []
|
||||
replaced = False
|
||||
|
||||
for line in lines:
|
||||
if not replaced and CommonUtils._parse_app_entry_from_line(line) == old_app:
|
||||
# Try to preserve indentation
|
||||
indent_idx = 0
|
||||
for char in line:
|
||||
if char.isspace():
|
||||
indent_idx += 1
|
||||
else:
|
||||
break
|
||||
indent = line[:indent_idx] if indent_idx > 0 else " "
|
||||
|
||||
new_lines.append(f'{indent}"{new_app}",')
|
||||
replaced = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if replaced:
|
||||
return "\n".join(new_lines)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def calculate_app_module_paths(app_names: list, metadata: dict) -> list:
|
||||
nested = bool(metadata.get("nested_apps"))
|
||||
nested_dir_name = metadata.get("nested_dir") if nested else None
|
||||
return [CommonUtils.calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names]
|
||||
|
||||
@staticmethod
|
||||
def calculate_app_module_path(app_name: str, nested: bool, nested_dir: str | None) -> str:
|
||||
return f"{nested_dir}.{app_name}" if nested and nested_dir else app_name
|
||||
|
||||
@staticmethod
|
||||
def get_full_app_config_path(app_name: str, nested: bool, nested_dir: str | None) -> str:
|
||||
"""Get the full path to the AppConfig class.
|
||||
Example: users -> users.apps.UsersConfig
|
||||
Example: (nested) apps.users -> apps.users.apps.UsersConfig
|
||||
"""
|
||||
module_path = CommonUtils.calculate_app_module_path(app_name, nested, nested_dir)
|
||||
# Handle cases where app_name might be a module path already
|
||||
short_name = app_name.split(".")[-1]
|
||||
config_name = short_name.title().replace("_", "") + "Config"
|
||||
return f"{module_path}.apps.{config_name}"
|
||||
|
||||
@staticmethod
|
||||
def is_django_project(directory: str = None) -> bool:
|
||||
if directory is None:
|
||||
directory = os.getcwd()
|
||||
manage_py_path = os.path.join(directory, "manage.py")
|
||||
return os.path.exists(manage_py_path)
|
||||
|
||||
@staticmethod
|
||||
def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]:
|
||||
if search_dir is None:
|
||||
search_dir = os.getcwd()
|
||||
|
||||
for item in os.listdir(search_dir):
|
||||
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
|
||||
candidate = os.path.join(search_dir, item)
|
||||
base_py = os.path.join(candidate, "settings", "base.py")
|
||||
if os.path.exists(base_py):
|
||||
return candidate, base_py
|
||||
return None, None
|
||||
|
||||
@staticmethod
|
||||
def find_settings_path(search_dir: str = None) -> str | None:
|
||||
project_dir, settings_base_path = CommonUtils.find_project_dir(search_dir)
|
||||
if settings_base_path:
|
||||
return os.path.dirname(settings_base_path)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_base_settings_path(project_root: str, project_name: str) -> str:
|
||||
settings_dir = os.path.join(project_root, project_name, "settings")
|
||||
return os.path.join(settings_dir, "base.py")
|
||||
|
||||
@staticmethod
|
||||
def read_base_settings(project_root: str, project_name: str) -> str | None:
|
||||
base_settings_path = CommonUtils.get_base_settings_path(project_root, project_name)
|
||||
if not os.path.exists(base_settings_path):
|
||||
return None
|
||||
|
||||
with open(base_settings_path) as f:
|
||||
return f.read()
|
||||
|
||||
@staticmethod
|
||||
def detect_nested_structure_from_settings(
|
||||
base_settings_path: str, search_dir: str = None
|
||||
) -> tuple[bool, str | None, str]:
|
||||
if search_dir is None:
|
||||
search_dir = os.getcwd()
|
||||
|
||||
if not os.path.exists(base_settings_path):
|
||||
return False, None, search_dir
|
||||
|
||||
try:
|
||||
with open(base_settings_path) as f:
|
||||
base_content = f.read()
|
||||
|
||||
for line in CommonUtils._iterate_user_apps_lines(base_content):
|
||||
app_label = CommonUtils._parse_app_entry_from_line(line)
|
||||
if app_label and "." in app_label:
|
||||
nested_dir_name = app_label.split(".", 1)[0]
|
||||
nested_path = os.path.join(search_dir, nested_dir_name)
|
||||
if os.path.isdir(nested_path):
|
||||
return True, nested_dir_name, nested_path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False, None, search_dir
|
||||
|
||||
@staticmethod
|
||||
def get_djinit_config(project_root: str = None) -> dict | None:
|
||||
"""Read .djinit configuration file."""
|
||||
import json
|
||||
|
||||
if project_root is None:
|
||||
project_root = os.getcwd()
|
||||
|
||||
config_path = os.path.join(project_root, ".djinit")
|
||||
if not os.path.exists(config_path):
|
||||
# Try finding it in parent directories
|
||||
parent = os.path.dirname(project_root)
|
||||
if parent and parent != project_root:
|
||||
return CommonUtils.get_djinit_config(parent)
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
Django helper module that replicates Django's startproject and startapp functionality.
|
||||
|
||||
This module provides functions to create Django projects and apps without requiring Django.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.core.config import DJANGO_VERSION
|
||||
from djinit.utils.common import CommonUtils
|
||||
|
||||
|
||||
class DjangoHelper:
|
||||
DJANGO_VERSION = DJANGO_VERSION
|
||||
|
||||
@staticmethod
|
||||
def startproject(project_name: str, directory: str, unified: bool = False, metadata: dict = None) -> None:
|
||||
try:
|
||||
from djinit.utils.secretkey import generate_secret_key
|
||||
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
manage_py_path = os.path.join(directory, "manage.py")
|
||||
CommonUtils.create_file_from_template(
|
||||
manage_py_path, "project/manage.py-tpl", {"project_name": project_name}, "Created manage.py"
|
||||
)
|
||||
os.chmod(manage_py_path, 0o755)
|
||||
|
||||
if unified:
|
||||
return
|
||||
|
||||
project_config_dir = os.path.join(directory, project_name)
|
||||
CommonUtils.create_directory_with_init(project_config_dir, f"Created {project_name}/__init__.py")
|
||||
|
||||
settings_dir = os.path.join(project_config_dir, "settings")
|
||||
CommonUtils.create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py")
|
||||
|
||||
# Generate secret key for development
|
||||
secret_key = generate_secret_key()
|
||||
metadata = metadata or {}
|
||||
|
||||
base_context = {
|
||||
"project_name": project_name,
|
||||
"app_names": [],
|
||||
"use_database_url": metadata.get("use_database_url", True),
|
||||
"database_type": metadata.get("database_type", "postgresql"),
|
||||
"use_tailwind": metadata.get("use_tailwind", False),
|
||||
"use_htmx": metadata.get("use_htmx", False),
|
||||
}
|
||||
|
||||
dev_context = {"secret_key": secret_key}
|
||||
|
||||
# Production context also needs database info
|
||||
prod_context = base_context.copy()
|
||||
|
||||
settings_files = [
|
||||
("base.py", "config/settings/base.py-tpl", base_context),
|
||||
("development.py", "config/settings/development.py-tpl", dev_context),
|
||||
("production.py", "config/settings/production.py-tpl", prod_context),
|
||||
]
|
||||
CommonUtils.create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/")
|
||||
|
||||
urls_context = {
|
||||
"url_type": "project",
|
||||
"project_name": project_name,
|
||||
"django_version": DjangoHelper.DJANGO_VERSION,
|
||||
}
|
||||
project_files = [
|
||||
("urls.py", "config/urls.py-tpl", urls_context),
|
||||
("wsgi.py", "config/wsgi.py-tpl", {"project_name": project_name}),
|
||||
("asgi.py", "config/asgi.py-tpl", {"project_name": project_name}),
|
||||
]
|
||||
CommonUtils.create_files_from_templates(project_config_dir, project_files, f"{project_name}/")
|
||||
|
||||
except Exception as e:
|
||||
from djinit.utils.exceptions import DjinitError
|
||||
|
||||
raise DjinitError(f"Error creating Django project: {e}", details=str(e)) from e
|
||||
|
||||
@staticmethod
|
||||
def startapp(app_name: str, directory: str, app_module: str = None) -> bool:
|
||||
"""Create a Django app with standard structure (empty files, no example code).
|
||||
|
||||
Args:
|
||||
app_name: Name of the app to create
|
||||
directory: Directory where the app should be created
|
||||
app_module: Full module path (e.g., 'apps.users' for nested apps)
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
app_dir = os.path.join(directory, app_name)
|
||||
os.makedirs(app_dir, exist_ok=True)
|
||||
|
||||
# Prepare context
|
||||
app_config_name = app_name.title().replace("_", "")
|
||||
context = {
|
||||
"app_name": app_name,
|
||||
"app_config_name": app_config_name,
|
||||
"django_version": DjangoHelper.DJANGO_VERSION,
|
||||
}
|
||||
|
||||
# Use provided app_module or default to app_name (path calculation handled by caller)
|
||||
if not app_module:
|
||||
app_module = app_name
|
||||
|
||||
context["app_module"] = app_module
|
||||
|
||||
# Create __init__.py
|
||||
CommonUtils.create_init_file(app_dir, f"Created {app_name}/__init__.py")
|
||||
|
||||
# Create all standard Django app files (empty templates with basic structure)
|
||||
app_files = [
|
||||
("apps.py", "components/apps.py-tpl", context),
|
||||
("models.py", "components/models.py-tpl", context),
|
||||
("views.py", "components/views.py-tpl", context),
|
||||
("admin.py", "components/admin.py-tpl", context),
|
||||
("serializers.py", "components/serializers.py-tpl", context),
|
||||
("urls.py", "components/urls.py-tpl", context),
|
||||
("tests.py", "components/tests.py-tpl", context),
|
||||
]
|
||||
|
||||
CommonUtils.create_files_from_templates(app_dir, app_files, f"{app_name}/")
|
||||
|
||||
# Create migrations directory
|
||||
migrations_dir = os.path.join(app_dir, "migrations")
|
||||
CommonUtils.create_directory_with_init(migrations_dir, f"Created {app_name}/migrations/__init__.py")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
from djinit.utils.exceptions import DjinitError
|
||||
|
||||
raise DjinitError(f"Error creating Django app: {e}", details=str(e)) from e
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -11,6 +11,27 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.4.0"
|
||||
@@ -25,9 +46,9 @@ name = "djinitx"
|
||||
version = "0.3.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "jinja2" },
|
||||
{ name = "questionary" },
|
||||
{ name = "rich" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -38,9 +59,9 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "jinja2", specifier = ">=3.1.0" },
|
||||
{ name = "questionary", specifier = ">=2.0.1" },
|
||||
{ name = "rich", specifier = ">=14.2.0" },
|
||||
{ name = "typer", specifier = ">=0.21.1" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -51,11 +72,11 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.0"
|
||||
version = "3.20.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -67,18 +88,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
@@ -91,91 +100,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
@@ -233,11 +157,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -355,6 +279,30 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.21.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
@@ -366,7 +314,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.35.4"
|
||||
version = "20.36.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
@@ -374,9 +322,9 @@ dependencies = [
|
||||
{ name = "platformdirs" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user