80 Commits
Author SHA1 Message Date
sankalp f7c5a99e41 chore: bump version to 0.3.2 and remove click dependency 2025-12-06 19:35:42 +05:45
sankalp 68f8cb81b5 chore: move dev dependencies to a separate group 2025-12-06 19:34:14 +05:45
sankalp 962fa3c504 chore: add questionary dependency for interactive prompts 2025-12-05 19:53:03 +05:45
sankalp f875893399 Merge branch 'main' of github.com:S4NKALP/djinit 2025-12-04 16:14:12 +05:45
sankalp cd1054b071 chore: bump version to 0.3.1 2025-12-04 16:13:16 +05:45
sankalp e60f5a4c1b Merge pull request #15 from S4NKALP/dependabot/pip/main/ruff-0.14.7
chore(deps): bump ruff from 0.14.6 to 0.14.7
2025-12-02 07:53:09 +05:45
sankalp 4d45581419 Merge pull request #14 from S4NKALP/dependabot/github_actions/main/actions/checkout-6
chore(deps): bump actions/checkout from 4 to 6
2025-12-02 07:52:56 +05:45
sankalp bde87b91a0 Merge pull request #13 from S4NKALP/dependabot/github_actions/main/actions/setup-python-6
chore(deps): bump actions/setup-python from 4 to 6
2025-12-02 07:52:42 +05:45
dependabot[bot] f008b08e8b chore(deps): bump ruff from 0.14.6 to 0.14.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.6 to 0.14.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.6...0.14.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.14.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:15:35 +00:00
dependabot[bot] 8d67e7831c chore(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:01:07 +00:00
dependabot[bot] 717fe0aec5 chore(deps): bump actions/setup-python from 4 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:01:00 +00:00
sankalp 00fa9ae5f5 chore: update uv.lock to require Python 3.10+ 2025-11-29 20:13:17 +05:45
sankalp 56d54c5f37 feat: improve project structure detection and interactive configuration 2025-11-29 20:10:30 +05:45
sankalp 1727633bd0 chore: update uv.lock to use editable installs 2025-11-28 01:55:49 +05:45
sankalp 356a4d7b0d chore: use python -m build instead of hatch build 2025-11-28 01:52:20 +05:45
sankalp 977be654fe chore: remove test job from publish workflow 2025-11-28 01:41:01 +05:45
sankalp 91e73ba96b chore: configure build system with hatchling 2025-11-28 01:24:05 +05:45
sankalp 63200dab4e chore: bump version to 0.3.0 2025-11-28 01:14:44 +05:45
sankalp 07112a5b57 chore: replace python-dotenv with django-environ and add mysqlclient 2025-11-28 01:07:34 +05:45
sankalp ca6af9ed2a refactor(core): improve error handling and remove boolean returns from functions 2025-11-28 01:03:27 +05:45
sankalp 03dfc78ee3 docs: add TODO file with future plans and enhancements 2025-11-27 22:09:56 +05:45
sankalp e559a87ca7 style(ui): improve console output formatting and spacing 2025-11-27 21:45:07 +05:45
sankalp 233ac71838 docs: improve README to clarify project setup steps and folder structures 2025-11-27 21:38:57 +05:45
sankalp 00a166fea6 refactor(ui): streamline input collection and project setup flow 2025-11-27 21:24:43 +05:45
sankalp 7937ac7134 refactor(config): replace python-dotenv with django-environ for settings management 2025-11-27 21:09:36 +05:45
sankalp 041cc5c155 feat(config): add database type selection and configuration 2025-11-27 21:06:40 +05:45
sankalp e5f568f275 docs: remove checkmark emojis from feature list for clarity 2025-11-27 00:41:32 +05:45
sankalp 8a38bc5228 docs: improve and expand README with detailed usage and features 2025-11-27 00:39:23 +05:45
sankalp 8a8e6ed556 refactor(ui): remove TimeRemainingColumn from progress bar for clarity 2025-11-27 00:31:28 +05:45
sankalp ce5f8da71c refactor: restructure project for improved modularity and testability 2025-11-27 00:29:27 +05:45
sankalp ac2b586c0d feat: add pre-commit support to project generation 2025-11-26 23:53:06 +05:45
sankalp a2cf00d637 docs: fix typo and ensure newline at end of README template 2025-11-26 23:32:38 +05:45
sankalp ec4f20d733 update 2025-11-26 23:19:52 +05:45
sankalp bea8965bbc feat: enhance predefined project structure with API and models READMEs and updated URLs 2025-11-26 22:59:47 +05:45
sankalp f22c4613f5 feat: add support for detecting and restricting app creation in unified/single folder structures 2025-11-26 15:08:13 +05:45
sankalp d82884e831 feat: enhance project template structure and file generation 2025-11-26 14:42:51 +05:45
sankalp eacd0c8b70 fix(cli): handle missing default profile gracefully 2025-11-26 14:24:56 +05:45
sankalp 5efea9b779 feat(ci): add workflow to publish package to PyPI on release 2025-11-26 14:24:26 +05:45
sankalp 201e54b7f9 chore: update djinitx version to 0.2.1 2025-11-25 23:40:11 +05:45
sankalp bf13a9524e feat(db): optimize SQLite settings for improved performance 2025-11-25 22:45:23 +05:45
sankalp 42cab7ef44 feat: add single folder structure option for project generation 2025-11-25 17:46:21 +05:45
sankalp 1a5b0c6f27 docs: update readme to reference djinit repository 2025-11-25 13:04:23 +05:45
sankalp be4d754b34 chore: bump version to 0.2.1 and remove django dependency 2025-11-24 20:17:45 +05:45
sankalp 93c7cd1da2 Merge pull request #12 from S4NKALP/dependabot/pip/main/ruff-0.14.6
chore(deps): bump ruff from 0.14.4 to 0.14.6
2025-11-24 14:19:50 +05:45
sankalp 28d298679e Merge pull request #10 from S4NKALP/dependabot/github_actions/main/actions/checkout-6
chore(deps): bump actions/checkout from 5 to 6
2025-11-24 14:19:35 +05:45
sankalp ad5bd8c699 Merge pull request #11 from S4NKALP/dependabot/pip/main/click-8.3.1
chore(deps): bump click from 8.3.0 to 8.3.1
2025-11-24 14:19:18 +05:45
dependabot[bot] 08645b9d90 chore(deps): bump ruff from 0.14.4 to 0.14.6
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.4 to 0.14.6.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.4...0.14.6)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.14.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 02:34:06 +00:00
dependabot[bot] 3f39e458c7 chore(deps): bump click from 8.3.0 to 8.3.1
Bumps [click](https://github.com/pallets/click) from 8.3.0 to 8.3.1.
- [Release notes](https://github.com/pallets/click/releases)
- [Changelog](https://github.com/pallets/click/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/click/compare/8.3.0...8.3.1)

---
updated-dependencies:
- dependency-name: click
  dependency-version: 8.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 02:33:57 +00:00
dependabot[bot] fe9c38e278 chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 02:29:32 +00:00
sankalp b7a8e20595 chore(asgi/wsgi): set default DJANGO_SETTINGS_MODULE for production environments 2025-11-13 02:33:16 +05:45
sankalp a575863ca0 feat(cors): configure CORS settings for development and production environments 2025-11-13 01:52:56 +05:45
sankalp 3aef01d4b5 chore: update .env.sample with new Discord bot token key 2025-11-09 19:45:51 +05:45
sankalp 6c882864b4 chore: remove django as a direct dependency 2025-11-09 15:34:20 +05:45
sankalp fc5de30cb7 docs: improve README with detailed features, structures, commands, and setup instructions 2025-11-09 15:30:56 +05:45
sankalp 211219247d Merge pull request #9 from S4NKALP/dependabot/pip/main/ruff-0.14.4
chore(deps): bump ruff from 0.14.2 to 0.14.4
2025-11-09 15:26:21 +05:45
sankalp 6fefe8d948 Merge pull request #6 from S4NKALP/dependabot/github_actions/main/actions/setup-python-6
chore(deps): bump actions/setup-python from 5 to 6
2025-11-09 15:25:54 +05:45
sankalp b59f4b3f6e Merge pull request #7 from S4NKALP/dependabot/github_actions/main/actions/checkout-5
chore(deps): bump actions/checkout from 4 to 5
2025-11-09 15:25:41 +05:45
sankalp a8ce63d285 chore: bump version to 0.2.0 2025-11-09 15:23:43 +05:45
dependabot[bot] 82d0af049a chore(deps): bump ruff from 0.14.2 to 0.14.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.2 to 0.14.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.2...0.14.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.14.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-09 09:35:46 +00:00
sankalp 78cfdb1cc0 Merge pull request #5 from S4NKALP/dev
v0.2.0
2025-11-09 15:19:49 +05:45
sankalp 84db52c906 chore: remove redundant labels from dependabot config 2025-11-09 15:17:33 +05:45
sankalp 30ac34478b refactor(structure): streamline project structure creation and file generation 2025-11-09 15:10:09 +05:45
sankalp cdaeede45a refactor: improve project creation and app generation for better compatibility 2025-11-09 14:26:00 +05:45
sankalp 33e87e7764 feat: add unified project structure option 2025-11-08 23:01:37 +05:45
sankalp 079dfff8a5 chore: remove UIFormatter call to prevent premature output 2025-11-08 21:58:46 +05:45
sankalp e6ce03fa3c docs(ui): update command instructions to reflect just commands 2025-11-08 21:53:20 +05:45
sankalp e34faae3a7 feat: enhance project setup with .env.sample, improved .gitignore, and app structure support 2025-11-08 21:51:18 +05:45
sankalp 21823fcc61 fix(cli): set default package name to 'backend' when project dir is '.' or empty 2025-11-08 21:46:35 +05:45
sankalp eb59e0c487 fix(justfile): escape Jinja2 variables to prevent premature evaluation 2025-11-08 21:43:20 +05:45
sankalp 105cd75595 feat: add predefined project structure option with core and users apps 2025-11-07 10:48:01 +05:45
sankalp 71195af155 feat: improve project creation and input handling for better user experience 2025-11-06 15:07:36 +05:45
dependabot[bot] 5fa2003ae1 chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 02:36:51 +00:00
dependabot[bot] a9e066e694 chore(deps): bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 02:36:46 +00:00
sankalp 5da43ab3a5 feat: add "generated with djinit" comment to template files 2025-11-03 01:08:21 +05:45
sankalp f45bacf682 fix(ui): improve clarity of user prompts during initialization 2025-11-03 00:53:49 +05:45
sankalp 5a6e51f52f style: remove trailing whitespace from templates 2025-11-03 00:25:46 +05:45
sankalp 022a5a90af refactor: streamline project setup and file generation 2025-11-03 00:20:21 +05:45
sankalp f7c9a0764f style: standardize prompt formatting and input handling 2025-11-02 13:47:12 +05:45
sankalp 462251059c refactor: improve project directory handling and subprocess execution for compatibility 2025-11-02 13:41:30 +05:45
sankalp 4369cdd44f docs: update README with PyPI package name and installation instructions 2025-10-29 13:56:24 +05:45
104 changed files with 3423 additions and 2249 deletions

No files matched your search

-6
View File
@@ -6,9 +6,6 @@ updates:
interval: "weekly"
target-branch: "main"
rebase-strategy: "auto"
labels:
- "dependencies"
- "github-actions"
- package-ecosystem: "pip"
directory: "/"
@@ -16,7 +13,4 @@ updates:
interval: "weekly"
target-branch: "main"
rebase-strategy: "auto"
labels:
- "dependencies"
- "python"
open-pull-requests-limit: 10
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
python-version: ["3.13"]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
+1 -8
View File
@@ -1,13 +1,10 @@
name: Dependabot Auto-Approve and Auto-Merge
on:
pull_request_target:
types: [opened, reopened, synchronize]
permissions:
contents: write
pull-requests: write
jobs:
dependabot:
if: github.actor == 'dependabot[bot]'
@@ -16,27 +13,23 @@ jobs:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2
- name: Approve PR
if: |
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN || secrets.GITHUB_TOKEN }}
script: |
await github.pulls.createReview({
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
event: 'APPROVE'
})
- name: Enable auto-merge (squash)
if: |
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
uses: peter-evans/enable-pull-request-automerge@v3
with:
token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN || secrets.GITHUB_TOKEN }}
merge-method: squash
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
+27
View File
@@ -0,0 +1,27 @@
name: Publish to PyPI
on:
release:
types: [published]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: python -m build
- name: Publish package to PyPI
run: python -m twine upload dist/* --verbose
-39
View File
@@ -1,39 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
build-and-publish:
name: Build and Publish to PyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: |
python -m build
- name: Check package
run: |
twine check dist/*
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
twine upload dist/*
+61 -145
View File
@@ -1,130 +1,26 @@
#--------------------------------------------------#
# The following was generated with gitignore.nvim: #
# The following was generated by djinit: #
#--------------------------------------------------#
# Gitignore for the following technologies: Python
# Byte-compiled / optimized / DLL files
__pycache__/
# Python
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
__pycache__/
*.egg-info/
.installed.cfg
dist/
build/
.eggs/
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
# Django
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
*.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
# Environment
.env
.venv
env/
@@ -133,45 +29,65 @@ ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
.project
.pydevproject
.settings/
.ropeproject/
# Rope project settings
.ropeproject
# OS
.DS_Store
*.DS_Store
Thumbs.db
desktop.ini
# mkdocs documentation
/site
# Django specific
media/
staticfiles/
staticfiles/
static_root/
/static/
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Testing
.coverage
htmlcov/
.pytest_cache/
.tox/
coverage.xml
*.cover
.hypothesis/
.cache
# Pyre type checker
.pyre/
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# pytype static type analyzer
.pytype/
# Node modules (if using frontend tools)
node_modules/
# Cython debug symbols
cython_debug/
# Secret keys
secret.txt
secrets.json
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Jupyter Notebook
.ipynb_checkpoints
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# Backup files
*.bak
*.tmp
*~
# ruff
.ruff_cache/
# Database
*.db
*.sql
# LSP config files
pyrightconfig.json
djinit.egg-info/
djinit.egg-info/*
# Cloud
.cloud
+203 -89
View File
@@ -1,141 +1,255 @@
# djinit
<p align="center">
<img src="https://img.shields.io/badge/Django-4.2%20%7C%205.1%20%7C%205.2-0C4B33?logo=django&logoColor=white" alt="Django">
<img src="https://img.shields.io/badge/Python-3.13%2B-3776AB?logo=python&logoColor=white" alt="Python">
<a href="https://github.com/S4NKALP/djinit/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
</p>
<div align="center">
A fast, interactive CLI to bootstrap a modern, production‑ready Django project in minutes — featuring split settings, DRF and JWT integration, OpenAPI docs, CORS, static file handling via WhiteNoise, Postgres‑friendly configuration, CI/CD templates, deployment helpers, and battle‑tested defaults that deliver a polished developer experience out of the box.
> PyPI didn't allow the original name, so you'll find it as **djinitx** on PyPI
- ✨ Split settings: `settings/base.py`, `settings/development.py`, `settings/production.py`
- 🧱 Optional nested apps layout (e.g., `apps/`)
- 🧩 App scaffolding: URLs, serializers, routes
- 🔗 Project URLs wired automatically
- 🧰 Utility files: `.gitignore`, `README.md`, `.env.sample`, `requirements.txt`, `pyproject.toml`
- 🚀 Deploy helpers: `Justfile`, `Procfile`, `runtime.txt`
- 🛠️ CI/CD: GitHub Actions and/or GitLab CI
- 🎨 Polished UX with `rich`
<img src="https://img.shields.io/pypi/v/djinitx?color=blue&label=PyPI&logo=pypi&logoColor=white" alt="PyPI">
<img src="https://img.shields.io/badge/Django-4.2%20%7C%205.1%20%7C%205.2-0C4B33?logo=django&logoColor=white" alt="Django">
<img src="https://img.shields.io/badge/Python-3.13%2B-3776AB?logo=python&logoColor=white" alt="Python">
<a href="https://github.com/S4NKALP/djinit/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
---
</div>
## Demo
**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 demo](docs/demo.gif)
## Why djinit?
Tip: If the GIF doesn't load, open `docs/demo.gif` or view it on the repository page.
Starting a Django project usually means spending hours setting up the same things: splitting settings for dev/prod, configuring DRF, adding JWT auth, setting up CORS, preparing for deployment. djinit does all of this for you with sensible defaults and lets you choose the project structure that fits your needs.
## Installation
Using pipx (recommended):
**Recommended** (using pipx):
```bash
pipx install git+https://github.com/S4NKALP/djinit
pipx install djinitx
```
Using pip (user space):
Or with pip:
```bash
pip install --user git+https://github.com/S4NKALP/djinit
pip install djinitx
```
From source:
Or with uv:
```bash
git clone https://github.com/S4NKALP/djinit
cd djinit
pip install -e .
uv tool install djinitx
```
Requirements: Python 3.13+
**Requirements**: Python 3.13+
## Quick start
## Getting Started
Run the interactive setup:
Just run:
```bash
djinit setup
# or
```
You can also use the shorter alias:
```bash
dj setup
```
Prompts include:
The tool will ask you a few questions:
- Project directory
- Django project name (used by `django-admin startproject`)
- Apps layout (flat vs nested package like `apps/`)
- App names (comma‑separated)
- CI/CD choice (GitHub, GitLab, both, none)
- Database config preference (`DATABASE_URL` vs individual params)
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)
2. **Project Setup**:
- Where to create it (use `.` for current directory)
- Project name (or directory name for Single Folder)
3. **Database Configuration**:
- Use `DATABASE_URL` (cleaner, recommended)
- Or separate DB variables
- Choose between PostgreSQL or MySQL
4. **Django Apps** (Standard Structure only):
- Whether to use an `apps/` folder
- Which apps to create
5. **CI/CD Pipeline**:
- GitHub Actions, GitLab CI, both, or skip it
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
- **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)
## Commands
- `djinit setup` or `dj setup` — Launch the interactive project generator
- `djinit app <names>` or `dj app <names>` — Create one or more Django apps in an existing project
- Examples:
- `djinit app users` or `dj app users`
- `djinit app users,products,orders` or `dj app users,products,orders`
- `djinit app users products orders` or `dj app users products orders`
- `djinit secret [--count N] [--length L]` or `dj secret [--count N] [--length L]` — Print random Django `SECRET_KEY` values
- Example: `djinit secret --count 5 --length 50`
### Create a Project
## What gets generated
```bash
djinit setup
```
- Django project and apps, then structure validation
- Settings package and split settings
- Base/development/production settings
- Utility files
- App files: `urls.py`, `serializers.py`, `routes.py`
- Project `urls.py` (auto‑includes your apps)
- Updated `wsgi.py`, `asgi.py`, `manage.py`
- `Justfile`, `Procfile`, `runtime.txt`
- CI/CD workflows (per your selection)
### Add Apps to Existing Project
### Output highlights
```bash
djinit app users products orders
```
- `config/settings/` → `__init__.py`, `base.py`, `development.py`, `production.py`
- `apps/<app>/` → `urls.py`, `serializers.py`, `routes.py`
- Project root → `.gitignore`, `README.md`, `.env.sample`, `requirements.txt`, `pyproject.toml`, `Justfile`, `Procfile`, `runtime.txt`
- CI/CD → `.github/workflows/` and/or `.gitlab-ci.yml`
This automatically creates the apps, adds them to `INSTALLED_APPS`, and wires up URLs.
Note: If you choose a nested apps package (e.g., `apps`), imports and `INSTALLED_APPS` entries are configured accordingly.
### Generate Secret Keys
## Included packages (by default)
```bash
djinit secret
```
Generated `requirements.txt` includes:
Need more? Use `--count 5` or change length with `--length 64`.
- Django
- python-dotenv
- django-jazzmin
- djangorestframework
- djangorestframework-simplejwt
- drf-spectacular
- django-cors-headers
- whitenoise
- psycopg2-binary
- gunicorn
- dj-database-url (when you opt into `DATABASE_URL`)
## Project Structures
These give you a ready‑to‑use stack with DRF, JWT auth, OpenAPI schema generation, CORS, static files in production, and Postgres support.
### Standard Structure
## Environment and database
The classic Django layout with split settings:
- A `.env.sample` is generated to get you started.
- If you opt into `DATABASE_URL`, `production.py` expects an environment variable like:
`postgres://user:password@host:port/database`
```
myproject/
├── manage.py
├── myproject/ # Config module
│ ├── settings/ # Split settings
│ ├── urls.py
│ └── wsgi.py
└── apps/ # Your apps (optional)
└── users/
```
### Single Folder Layout
Simple structure with all apps in one folder:
```
myproject/
├── manage.py
├── project/ # Configurable folder name
│ ├── settings/
│ ├── urls.py
│ ├── models/ # All models here
│ ├── api/ # API views and serializers
│ │ └── your_model_name/
│ │ ├── views.py
│ │ ├── serializers.py
│ │ └── urls.py
│ └── wsgi.py
```
### Predefined Structure
Organized for larger projects:
```
myproject/
├── manage.py
├── config/ # Django config
│ ├── settings/
│ └── urls.py
├── apps/ # Business logic
│ ├── users/
│ └── core/
└── api/ # API routes
└── v1/
```
### Unified Structure
Clean and minimal:
```
myproject/
├── manage.py
├── core/ # Django config
│ ├── settings/
│ └── urls.py
└── apps/ # Main application package
├── admin/
├── models/
├── serializers/
├── views/
├── urls/
└── api/ # API routes
```
## Development Workflow
Your project comes with a `Justfile` for common tasks:
```bash
just dev # Start dev server
just migrate # Run migrations
just makemigrations # Create migrations
just shell # Django shell
just test # Run tests
just format # Format code
just lint # Lint code
```
Don't have `just` installed? No problem—these are just shortcuts for standard 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)
### API Endpoints
- `/admin/` - Django admin
- `/token/` - Get JWT token
- `/token/refresh/` - Refresh token
- `/docs/` - Swagger UI (dev only)
- `/schema/` - OpenAPI schema (dev only)
### Settings
**Development**: SQLite, debug mode, console emails, permissive CORS
**Production**: PostgreSQL, security hardened, SMTP emails, strict CORS
## Environment Setup
Copy `.env.sample` to `.env` and fill in your values:
```bash
SECRET_KEY=your-secret-key-here # Use: 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!
## Contributing
Contributions are welcome!
Please open an issue for bugs/ideas. PRs with clear descriptions are appreciated.
## Acknowledgments
- Django and the Django community
- Jinja2
- rich
- click
- ruff
Found a bug or have an idea? Open an issue or submit a pull request. Contributions are always welcome!
## License
+15
View File
@@ -0,0 +1,15 @@
# TODO - Future Plans
## Features
- [ ] Docker Support: Auto generating `Dockerfile` so you can containerize easily
- [ ] Frontend Integration: Options to scaffold React, Vue, or HTMX right alongside Django
- [ ] Celery: Making background tasks easier to set up
- [ ] More Packages: Integrating other popular packages to help you build feature rich apps effortlessly
## Enhancements
- [ ] Add more project structure templates
- [x] Add interactive configuration wizard
- [ ] Add testing framework setup (pytest, coverage)
- [x] Improve structure detection
+73 -62
View File
@@ -6,13 +6,9 @@ Coordinates between different managers to create a complete Django project.
import os
from typing import Callable, List, Tuple
from djinit.scripts.cicd_config_generator import CICDConfigGenerator
from djinit.scripts.console_ui import UIFormatter
from djinit.scripts.deployment_files_generator import DeploymentConfigGenerator
from djinit.scripts.files import FileManager
from djinit.scripts.project import ProjectManager
from djinit.scripts.secretkey_generator import generate_secret_command
from djinit.scripts.settings import SettingsManager
from djinit.services.files import FileManager
from djinit.services.project import ProjectManager
from djinit.ui.console import UIFormatter
class Cli:
@@ -22,88 +18,103 @@ class Cli:
self.primary_app = primary_app
self.app_names = app_names
self.metadata = metadata
self.project_root = os.path.join(os.getcwd(), project_dir)
if project_dir == ".":
self.project_root = os.getcwd()
else:
self.project_root = os.path.join(os.getcwd(), project_dir)
# Initialize managers
self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata)
self.settings_manager = SettingsManager(self.project_root, project_name, app_names, metadata)
self.file_manager = FileManager(self.project_root, project_name, app_names, metadata)
self.deployment_manager = DeploymentConfigGenerator(self.project_root, project_name)
self.cicd_manager = CICDConfigGenerator(self.project_root, project_name)
def _normalize_metadata(self) -> None:
"""Normalize metadata and app names based on structure type."""
if self.metadata.get("predefined_structure"):
self.metadata.setdefault("project_module_name", "config")
self.metadata.setdefault("nested_apps", True)
self.metadata.setdefault("nested_dir", "apps")
if not self.app_names:
self.app_names = ["users", "core"]
self.project_manager.app_names = self.app_names
elif self.metadata.get("unified_structure"):
self.metadata.setdefault("project_module_name", "core")
self.metadata.setdefault("nested_apps", True)
self.metadata.setdefault("nested_dir", "apps")
if not self.app_names:
self.app_names = []
self.project_manager.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
def run_setup(self) -> bool:
steps: List[Tuple[str, Callable[[], bool]]] = [
("Creating Django project", self.project_manager.create_project),
("Creating Django apps", self.project_manager.create_apps),
("Validating project structure", self.project_manager.validate_project_structure),
("Setting up project structure", self.settings_manager.create_settings_structure),
("Configuring base settings", self.settings_manager.update_base_settings),
("Configuring development settings", self.settings_manager.update_development_settings),
("Configuring production settings", self.settings_manager.update_production_settings),
("Creating utility files", self._create_utility_files),
("Setting up app URLs", self.file_manager.create_app_urls),
("Creating app serializers", self.file_manager.create_app_serializers),
("Creating app routes", self.file_manager.create_app_routes),
("Configuring comprehensive URLs", self.file_manager.update_project_urls),
("Updating WSGI configuration", self.file_manager.update_wsgi_file),
("Updating ASGI configuration", self.file_manager.update_asgi_file),
("Updating manage.py", self.file_manager.update_manage_py),
("Creating Justfile", self.deployment_manager.create_justfile),
("Creating Procfile", self.deployment_manager.create_procfile),
("Creating runtime.txt", lambda: self.deployment_manager.create_runtime_txt("3.13")),
("Creating CI/CD pipelines", self._create_cicd_pipelines),
]
self._normalize_metadata()
steps: List[Tuple[str, Callable[[], None]]] = []
steps.append(("Creating Django project", self.project_manager.create_project))
if self.metadata.get("unified_structure"):
steps.append(("Creating unified structure", self.file_manager.create_unified_structure))
elif self.metadata.get("single_structure"):
steps.append(("Creating single folder structure", self.file_manager.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))
else:
steps.append(("Creating Django apps", self.project_manager.create_apps))
steps.append(("Creating project URLs", self.file_manager.create_project_urls))
steps.extend(
[
("Validating project structure", self.project_manager.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 CI/CD pipelines", self._create_cicd_pipelines),
]
)
total_steps = len(steps)
total_steps = len(steps)
success = True
# Create live progress display
progress, task = UIFormatter.create_live_progress(description="Setup Progress", total_steps=total_steps)
with progress:
# Execute each step with progress tracking
for step_number, (description, step_func) in enumerate(steps, 1):
result = step_func()
if not result:
success = False
UIFormatter.print_error(f"Step {step_number} failed: {description}")
break
try:
with progress:
for step_number, (_, step_func) in enumerate(steps, 1):
step_func()
progress.update(task, advance=1, description=f"Step {step_number}/{total_steps}")
# Update the same progress bar
progress.update(task, advance=1, description=f"Step {step_number}/{total_steps}")
except Exception as e:
success = False
UIFormatter.handle_exception(e)
return success
def _create_utility_files(self) -> bool:
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),
]
for step_func in utility_steps:
result = step_func()
if not result:
return False
step_func()
UIFormatter.print_success("Created all utility files successfully!")
return True
def _create_cicd_pipelines(self) -> bool:
def _create_cicd_pipelines(self) -> None:
if self.metadata.get("use_github_actions", True):
result = self.cicd_manager.create_github_actions()
if not result:
return False
self.file_manager.create_github_actions()
if self.metadata.get("use_gitlab_ci", True):
result = self.cicd_manager.create_gitlab_ci()
if not result:
return False
return True
def generate_secret_keys(self) -> bool:
generate_secret_command()
return True
self.file_manager.create_gitlab_ci()
File renamed without changes.
@@ -6,8 +6,13 @@ Handles different commands like secret key generation and app creation.
import argparse
import sys
from djinit.scripts.console_ui import UIColors, UIFormatter, console
from djinit.scripts.name_validator import validate_app_name
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:
@@ -15,17 +20,14 @@ def parse_arguments() -> argparse.Namespace:
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# Setup command (default)
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 command
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 command
app_parser = subparsers.add_parser("app", help="Create one or more Django apps (comma or space separated)")
app_parser.add_argument(
"app_name",
@@ -43,19 +45,10 @@ def parse_arguments() -> argparse.Namespace:
def handle_secret_command(args: argparse.Namespace) -> None:
from djinit.scripts.secretkey_generator import display_secret_keys, generate_multiple_keys
# Generate keys
keys = generate_multiple_keys(args.count, args.length)
# Display results
UIFormatter.print_info("")
display_secret_keys(keys)
# Show usage instructions
from rich.panel import Panel
from rich.text import Text
instructions = Text()
instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT)
instructions.append("1. Copy the appropriate secret key for your environment\n")
@@ -70,47 +63,28 @@ def handle_secret_command(args: argparse.Namespace) -> None:
def handle_app_command(args: argparse.Namespace) -> None:
from djinit.scripts.app_generator import AppManager
# Display welcome message
UIFormatter.print_info("")
UIFormatter.print_header("Django App Creation")
UIFormatter.print_info("")
# Parse comma-separated names and validate
# args.app_name is a list due to nargs="+"; split each token by comma
tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
pieces = []
for token in tokens:
if token is None:
continue
for part in str(token).split(","):
part = part.strip()
if part:
pieces.append(part)
app_names = pieces
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 no comma, keep single name behavior
if not app_names:
UIFormatter.print_error("App name cannot be empty")
sys.exit(1)
# Deduplicate while preserving order
seen = set()
deduped_names = []
for name in app_names:
if name not in seen:
deduped_names.append(name)
seen.add(name)
deduped_names = list(dict.fromkeys(app_names))
# Validate all names first
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)
# Create apps sequentially
any_failure = False
for name in deduped_names:
app_manager = AppManager(name)
@@ -124,16 +98,12 @@ def handle_app_command(args: argparse.Namespace) -> None:
if any_failure:
sys.exit(1)
# Show next steps
from rich.panel import Panel
from rich.text import Text
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: python manage.py makemigrations\n")
instructions.append("4. Apply migrations: python manage.py migrate\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"))
View File
Whitespace-only changes.
+14
View File
@@ -0,0 +1,14 @@
"""
Core configuration and constants for djinit.
"""
import sys
# Django version used for project generation
DJANGO_VERSION = "5.2"
# Reserved names that cannot be used for projects or apps
DJANGO_RESERVED = {"django", "test", "site-packages", "admin"}
# Python builtin module names
PYTHON_BUILTINS = set(sys.builtin_module_names)
+48
View File
@@ -0,0 +1,48 @@
"""
Core data structures and types for djinit.
"""
from dataclasses import dataclass
from typing import Tuple
@dataclass
class ProjectMetadata:
package_name: str
use_github_actions: bool = False
use_gitlab_ci: bool = False
nested_apps: bool = False
nested_dir: str | None = None
use_database_url: bool = False
database_type: str = "postgresql"
predefined_structure: bool = False
unified_structure: bool = False
single_structure: bool = False
project_module_name: str | None = None
def to_dict(self) -> dict:
return {
"package_name": self.package_name,
"use_github_actions": self.use_github_actions,
"use_gitlab_ci": self.use_gitlab_ci,
"nested_apps": self.nested_apps,
"nested_dir": self.nested_dir,
"use_database_url": self.use_database_url,
"database_type": self.database_type,
"predefined_structure": self.predefined_structure,
"unified_structure": self.unified_structure,
"single_structure": self.single_structure,
"project_module_name": self.project_module_name,
}
@dataclass
class ProjectSetup:
project_dir: str
project_name: str
primary_app: str
app_names: list[str]
metadata: ProjectMetadata
def to_tuple(self) -> Tuple[str, str, str, list, dict]:
return (self.project_dir, self.project_name, self.primary_app, self.app_names, self.metadata.to_dict())
+6 -19
View File
@@ -8,54 +8,44 @@ import subprocess
import sys
from djinit.cli import Cli
from djinit.scripts.command_handler import handle_app_command, handle_secret_command, parse_arguments
from djinit.scripts.console_ui import UIFormatter, console
from djinit.scripts.input_handler import confirm_setup, get_user_input
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) # Windows
subprocess.run("cls", shell=True, check=True)
else:
subprocess.run("clear", shell=True, check=True) # Linux/MacOS
subprocess.run("clear", shell=True, check=True)
except subprocess.CalledProcessError:
# If clearing fails, just print some newlines
console.print("\n" * 50)
def main() -> None:
# Parse command line arguments
args = parse_arguments()
# Handle secret command
if args.command == "secret":
handle_secret_command(args)
return
# Handle app command
if args.command == "app":
handle_app_command(args)
return
# Default to setup command
# Clear screen
clear_screen()
# Display welcome screen
console.print()
console.print(UIFormatter.create_welcome_panel())
console.print()
# Get user input
project_dir, project_name, primary_app, app_names, metadata = get_user_input()
# Confirm setup
if not confirm_setup(project_dir, project_name, app_names, metadata):
UIFormatter.print_info("Setup cancelled by user.")
return
# Run setup
console.print()
UIFormatter.print_info("Starting Django project setup...")
console.print()
@@ -63,10 +53,7 @@ def main() -> None:
django_cli = Cli(project_dir, project_name, primary_app, app_names, metadata)
success = django_cli.run_setup()
# Display completion summary
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True)
# Exit with appropriate code
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True, metadata=metadata)
sys.exit(0 if success else 1)
-254
View File
@@ -1,254 +0,0 @@
"""
App management for djinit.
Handles creation of Django apps and updating settings.
"""
import os
import subprocess
from typing import Optional
from djinit.scripts.console_ui import UIFormatter
from djinit.scripts.template_engine import template_engine
from djinit.utils import change_cwd, create_file_with_content
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")
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._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
if not self._create_additional_files():
return False
UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!")
return True
def _is_django_project(self) -> bool:
return os.path.exists(self.manage_py_path)
def _app_exists(self) -> bool:
# Detect project structure to check in correct location
is_nested, nested_dir, apps_base_dir = self._detect_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:
# Detect project structure
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
# manage.py is always in the project root (current_dir), not in the project directory
manage_py_path = os.path.join(self.current_dir, "manage.py")
if not os.path.exists(manage_py_path):
UIFormatter.print_error("Could not find manage.py file in project root")
return False
# Create app in the correct location
with change_cwd(apps_base_dir):
subprocess.run(["django-admin", "startapp", self.app_name], capture_output=True, text=True, check=True)
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
return True
def _add_to_installed_apps(self) -> bool:
settings_path = self._find_settings_path()
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()
# Detect project structure to determine the correct app module path
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
# Determine the correct module path for the app
if is_nested and nested_dir:
app_module_path = f"{nested_dir}.{self.app_name}"
else:
app_module_path = self.app_name
# Check if app is already in USER_DEFINED_APPS section specifically
lines = content.split("\n")
in_user_apps = False
app_already_exists = False
for line in lines:
if "USER_DEFINED_APPS" in line and "=" in line:
in_user_apps = True
elif in_user_apps and line.strip() == "]":
in_user_apps = False
elif in_user_apps and (f'"{app_module_path}"' in line or f"'{app_module_path}'" in line):
app_already_exists = True
break
if app_already_exists:
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
lines = content.split("\n")
new_lines = []
in_user_apps = False
apps_added = False
for line in lines:
if "USER_DEFINED_APPS" in line and "=" in line:
in_user_apps = True
new_lines.append(line)
elif in_user_apps and line.strip() == "]":
new_lines.append(f' "{app_module_path}",')
new_lines.append(line)
in_user_apps = False
apps_added = True
else:
new_lines.append(line)
if not apps_added:
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
return False
with open(base_settings_path, "w") as f:
f.write("\n".join(new_lines))
UIFormatter.print_success(f"Added '{app_module_path}' to USER_DEFINED_APPS in base.py")
return True
def _find_settings_path(self) -> Optional[str]:
# Look for project directories that contain settings
for item in os.listdir(self.current_dir):
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
# Check if this directory contains Django project files
project_path = os.path.join(self.current_dir, item)
settings_path = os.path.join(project_path, "settings")
# Check if settings directory exists with base.py
if os.path.exists(settings_path) and os.path.exists(os.path.join(settings_path, "base.py")):
return settings_path
# Fallback: look for settings in current directory
settings_path = os.path.join(self.current_dir, "settings")
if os.path.exists(settings_path) and os.path.exists(os.path.join(settings_path, "base.py")):
return settings_path
return None
def _detect_project_structure(self) -> tuple[bool, Optional[str], str]:
# Step 1: find project directory containing settings/base.py
project_dir: Optional[str] = None
settings_base_path: Optional[str] = None
for item in os.listdir(self.current_dir):
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
candidate = os.path.join(self.current_dir, item)
base_py = os.path.join(candidate, "settings", "base.py")
if os.path.exists(base_py):
project_dir = candidate
settings_base_path = base_py
break
# Fallback: current_dir/settings/base.py
if project_dir is None:
base_py = os.path.join(self.current_dir, "settings", "base.py")
if os.path.exists(base_py):
project_dir = self.current_dir
settings_base_path = base_py
if project_dir is None:
# Not a Django project structure
return False, None, self.current_dir
# Step 2: try to infer nested_dir from USER_DEFINED_APPS in base.py
try:
if settings_base_path and os.path.exists(settings_base_path):
with open(settings_base_path) as f:
base_content = f.read()
# Find lines inside USER_DEFINED_APPS and extract first dotted app
in_user_apps = False
for line in base_content.splitlines():
if "USER_DEFINED_APPS" in line and "=" in line:
in_user_apps = True
continue
if in_user_apps and "]" in line:
break
if in_user_apps:
line_stripped = line.strip().strip(",")
if line_stripped.startswith('"') or line_stripped.startswith("'"):
app_label = line_stripped.strip("\"'")
if "." in app_label:
prefix = app_label.split(".", 1)[0]
nested_path = os.path.join(self.current_dir, prefix)
if os.path.isdir(nested_path):
return True, prefix, nested_path
except Exception:
# Ignore parsing issues and continue with heuristics
pass
# Step 3: look for existing package directories in project root
preferred = ["apps", "modules", "packages"]
for name in preferred:
nested_path = os.path.join(self.current_dir, name)
if os.path.isdir(nested_path) and os.path.exists(os.path.join(nested_path, "__init__.py")):
return True, name, nested_path
# Any other directory with __init__.py qualifies as a package
for item in os.listdir(self.current_dir):
if item in ("__pycache__",) or item.startswith("."):
continue
nested_path = os.path.join(self.current_dir, item)
if os.path.isdir(nested_path) and os.path.exists(os.path.join(nested_path, "__init__.py")):
return True, item, nested_path
# Step 4: fallback
return False, None, self.current_dir
def _create_additional_files(self) -> bool:
"""Create additional files for the app (serializers.py, routes.py)."""
# Detect project structure to get correct app path
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
app_path = os.path.join(apps_base_dir, self.app_name)
# Create serializers.py
context = {"app_name": self.app_name}
serializers_content = template_engine.render_template("serializers.j2", context)
create_file_with_content(
os.path.join(app_path, "serializers.py"),
serializers_content,
f"Created {self.app_name}/serializers.py",
should_format=True,
)
# Create routes.py
routes_content = template_engine.render_template("routes.j2", context)
create_file_with_content(
os.path.join(app_path, "routes.py"),
routes_content,
f"Created {self.app_name}/routes.py",
should_format=True,
)
return True
-41
View File
@@ -1,41 +0,0 @@
"""
CI/CD pipeline manager for djinit
Handles creation of Github Actions and GitLab CI onfigurations
"""
import os
from djinit.scripts.template_engine import template_engine
from djinit.utils import change_cwd, create_file_with_content
class CICDConfigGenerator:
def __init__(self, project_root: str, project_name: str):
self.project_root = project_root
self.project_name = project_name
def create_github_actions(self) -> bool:
with change_cwd(self.project_root):
github_dir = os.path.join(self.project_root, ".github", "workflows")
os.makedirs(github_dir, exist_ok=True)
context = {"project_name": self.project_name}
workflow_content = template_engine.render_template("github_actions_ci.j2", context)
workflow_file = os.path.join(github_dir, "ci.yml")
create_file_with_content(
workflow_file,
workflow_content,
"Created Github Actions workflow (ci.yml)",
)
return True
def create_gitlab_ci(self) -> bool:
with change_cwd(self.project_root):
context = {"project_name": self.project_name}
gitlab_ci_content = template_engine.render_template("gitlab_ci.j2", context)
create_file_with_content(
".gitlab-ci.yml",
gitlab_ci_content,
"Created GitLab CI configuration (.gitlab-ci.yml)",
)
return True
-381
View File
@@ -1,381 +0,0 @@
"""
Console UI utilities for djinit.
Provides enhanced styling, formatting, and user interface components.
"""
from contextlib import contextmanager
from typing import Any, Dict, List, Optional
from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.progress import (
BarColumn,
Progress,
SpinnerColumn,
TaskProgressColumn,
TextColumn,
TimeRemainingColumn,
)
from rich.syntax import Syntax
from rich.table import Table
from rich.text import Text
console = Console()
class UIColors:
SUCCESS = "bold green"
ERROR = "bold red"
WARNING = "bold yellow"
INFO = "bold blue"
HIGHLIGHT = "bold cyan"
MUTED = "dim white"
ACCENT = "bold magenta"
PRIMARY = "bold white"
SECONDARY = "bright_white"
CODE = "dim cyan"
GRADIENT_START = "rgb(139,92,246)"
GRADIENT_END = "rgb(236,72,153)"
class Icons:
SUCCESS = "✓"
ERROR = "✗"
WARNING = "⚠"
INFO = "ℹ"
ROCKET = "🚀"
PACKAGE = "📦"
FOLDER = "📁"
FILE = "📄"
SETTINGS = "⚙"
DATABASE = "🗄"
LINK = "🔗"
STAR = "⭐"
FIRE = "🔥"
SPARKLES = "✨"
PARTY = "🎉"
TARGET = "🎯"
MAGNIFIER = "🔍"
WRENCH = "🔧"
CLOCK = "⏱"
class UIFormatter:
"""Utility class for consistent UI formatting"""
@staticmethod
def print_success(message: str, icon: str = Icons.SUCCESS):
console.print(f"[{UIColors.SUCCESS}]{icon}[/{UIColors.SUCCESS}] [bold]{message}[/bold]")
@staticmethod
def print_error(message: str, icon: str = Icons.ERROR, details: Optional[str] = None):
"""Print error message with optional details"""
console.print(f"[{UIColors.ERROR}]{icon}[/{UIColors.ERROR}] [bold]{message}[/bold]")
if details:
console.print(f" [dim]{details}[/dim]")
@staticmethod
def print_warning(message: str, icon: str = Icons.WARNING):
console.print(f"[{UIColors.WARNING}]{icon}[/{UIColors.WARNING}] [bold]{message}[/bold]")
@staticmethod
def print_info(message: str, icon: str = Icons.INFO):
console.print(f"[{UIColors.INFO}]{icon}[/{UIColors.INFO}] [bold]{message}[/bold]")
@staticmethod
def print_step(step_number: int, total_steps: int, description: str, icon: str = ""):
"""Print step information with enhanced progress indicator"""
icon_str = f"{icon} " if icon else ""
percentage = int((step_number / total_steps) * 100)
console.print(
f"[{UIColors.HIGHLIGHT}]Step {step_number}/{total_steps}[/{UIColors.HIGHLIGHT}] "
f"[dim]({percentage}%)[/dim] {icon_str}{description}"
)
@staticmethod
def print_progress_bar(step_number: int, total_steps: int, width: int = 40):
"""Print a single progress bar that shows overall progress"""
progress_percentage = int((step_number / total_steps) * 100)
filled = int((step_number / total_steps) * width)
bar = "█" * filled + "░" * (width - filled)
console.print(
f"\r[{UIColors.ACCENT}][{bar}][/{UIColors.ACCENT}] {progress_percentage}%",
end="",
)
@staticmethod
def create_live_progress(description: str = "Setup Progress", total_steps: int = 100):
"""Create a live progress display that updates in place"""
progress = Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(bar_width=40),
TaskProgressColumn(),
TimeRemainingColumn(),
console=console,
)
task = progress.add_task(description, total=total_steps)
return progress, task
@staticmethod
def print_header(text: str, style: str = UIColors.PRIMARY, width: int = 70):
"""Print a styled header with customizable width"""
console.print(f"\n[{style}]{'═' * width}[/{style}]")
console.print(f"[{style}]{text.center(width)}[/{style}]")
console.print(f"[{style}]{'═' * width}[/{style}]\n")
@staticmethod
def print_subheader(text: str, style: str = UIColors.HIGHLIGHT):
"""Print a styled subheader"""
console.print(f"\n[{style}]{text}[/{style}]")
console.print(f"[{UIColors.MUTED}]{'─' * len(text)}[/{UIColors.MUTED}]")
@staticmethod
def print_separator(char: str = "─", width: int = 70, style: str = UIColors.MUTED):
"""Print a visual separator with customization"""
console.print(f"[{style}]{char * width}[/{style}]")
@staticmethod
def print_feature_list(features: List[str], icon: str = Icons.STAR):
"""Print a styled feature list with custom icons"""
for _i, feature in enumerate(features, 1):
console.print(f"[{UIColors.ACCENT}] {icon}[/{UIColors.ACCENT}] [bold]{feature}[/bold]")
@staticmethod
def print_numbered_list(items: List[str], start: int = 1):
"""Print a numbered list with tree-style formatting"""
total = len(items)
for i, item in enumerate(items, start):
prefix = "└─" if i == start + total - 1 else "├─"
console.print(f" [dim]{prefix}[/dim] [dim]{i}.[/dim] {item}")
@staticmethod
def print_tree_item(text: str, is_last: bool = False, indent: int = 0, icon: str = ""):
"""Print a tree-style item"""
prefix = "└─" if is_last else "├─"
icon_str = f"{icon} " if icon else ""
spacing = " " * indent
console.print(f"{spacing}[dim]{prefix}[/dim] {icon_str}{text}")
@staticmethod
def print_code_block(
code: str,
language: str = "python",
theme: str = "monokai",
line_numbers: bool = True,
):
"""Print code with syntax highlighting and customization"""
syntax = Syntax(code, language, theme=theme, line_numbers=line_numbers, word_wrap=True)
console.print(syntax)
@staticmethod
def print_panel(content: str, title: str = "", style: str = "cyan", border_style: str = "blue"):
"""Print content in a styled panel"""
panel = Panel(
content,
title=title,
border_style=border_style,
box=box.ROUNDED,
padding=(1, 2),
)
console.print(panel)
@staticmethod
def print_table(data: List[Dict[str, Any]], title: Optional[str] = None):
"""Print data in a styled table"""
if not data:
return
table = Table(title=title, box=box.ROUNDED, show_header=True, header_style="bold cyan")
# Add columns from first row
for key in data[0].keys():
table.add_column(key.replace("_", " ").title(), style="white")
# Add rows
for row in data:
table.add_row(*[str(v) for v in row.values()])
console.print(table)
@staticmethod
def create_welcome_panel():
"""Create an enhanced welcome panel with ASCII art header"""
welcome_text = Text()
ascii_art = r"""
_ _ _ _ _
| (_) (_) (_) |
__| |_ __ _ _ __ __ _ ___ _ _ __ _| |_
/ _` | |/ _` | '_ \ / _` |/ _ \ | | '_ \| | __|
| (_| | | (_| | | | | (_| | (_) | | | | | | | |_
\__,_| |\__,_|_| |_|\__, |\___/ |_|_| |_|_|\__|
_/ | __/ |
|__/ |___/
"""
welcome_text.append(ascii_art, style=UIColors.ACCENT)
welcome_text.append("\n Django Project Setup Tool\n", style="bold white")
welcome_text.append(
" Create production-ready Django projects with modern architecture\n\n",
style=UIColors.MUTED,
)
welcome_text.append(" Repository: ", style=UIColors.MUTED)
welcome_text.append("https://github.com/S4NKALP/djinit\n", style="blue underline")
welcome_text.append(" License: MIT\n", style=UIColors.MUTED)
return welcome_text
@staticmethod
def create_summary_panel(
project_dir: str,
project_name: str,
app_names: List[str],
success: bool,
duration: Optional[float] = None,
):
"""Create a clean completion summary with optional duration"""
console.print("\n")
if success:
# Success header
console.print(f"[bold green]{'═' * 70}[/bold green]")
console.print(f"[bold green]{f'{Icons.PARTY} SETUP COMPLETE! {Icons.PARTY}'.center(70)}[/bold green]")
console.print(f"[bold green]{'═' * 70}[/bold green]\n")
# Duration if provided
if duration:
console.print(f"[dim]{Icons.CLOCK} Completed in {duration:.2f} seconds[/dim]\n")
# Project details
console.print(f"[bold cyan]{Icons.TARGET} Project Details:[/bold cyan]")
console.print(" [dim]│[/dim]")
console.print(f" [dim]├─[/dim] [bold]Directory:[/bold] [white]{project_dir}[/white]")
console.print(f" [dim]├─[/dim] [bold]Project:[/bold] [white]{project_name}[/white]")
console.print(f" [dim]└─[/dim] [bold]Apps:[/bold] [white]{', '.join(app_names)}[/white]")
console.print()
# Next steps
console.print(f"[bold cyan]{Icons.ROCKET} Next Steps:[/bold cyan]")
console.print(" [dim]│[/dim]")
console.print(" [dim]├─[/dim] [dim]1.[/dim] Navigate to your project directory")
console.print(" [dim]├─[/dim] [dim]2.[/dim] Set environment variables in .env file")
console.print(" [dim]├─[/dim] [dim]3.[/dim] Run migrations: [cyan]python manage.py migrate[/cyan]")
console.print(
" [dim]├─[/dim] [dim]4.[/dim] Create superuser: [cyan]python manage.py createsuperuser[/cyan]"
)
console.print(" [dim]└─[/dim] [dim]5.[/dim] Start server: [cyan]python manage.py runserver[/cyan]")
console.print()
# Useful URLs
console.print(f"[bold cyan]{Icons.LINK} Useful URLs:[/bold cyan]")
console.print(" [dim]│[/dim]")
console.print(" [dim]├─[/dim] Admin: [blue]http://localhost:8000/admin/[/blue]")
console.print(" [dim]├─[/dim] API Docs: [blue]http://localhost:8000/docs/[/blue]")
console.print(" [dim]└─[/dim] API Schema: [blue]http://localhost:8000/schema/[/blue]")
console.print()
console.print(f"[bold green]{'═' * 70}[/bold green]\n")
else:
# Error header
console.print(f"[bold red]{'═' * 70}[/bold red]")
console.print(f"[bold red]{'❌ SETUP FAILED'.center(70)}[/bold red]")
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
console.print("[red]The setup process encountered an error.[/red]\n")
# Troubleshooting
console.print(f"[bold yellow]{Icons.MAGNIFIER} Troubleshooting Tips:[/bold yellow]")
console.print(" [dim]│[/dim]")
console.print(" [dim]├─[/dim] Check the error messages above")
console.print(" [dim]├─[/dim] Ensure you have write permissions")
console.print(" [dim]├─[/dim] Verify Django is installed correctly")
console.print(" [dim]└─[/dim] Try running with elevated permissions")
console.print()
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
@staticmethod
def create_progress_display(description: str = "Processing", total_steps: int = 100):
"""Create a comprehensive live progress display"""
progress = Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(bar_width=40),
TaskProgressColumn(),
TimeRemainingColumn(),
console=console,
)
return progress
@staticmethod
@contextmanager
def status(message: str, spinner: str = "dots"):
"""Context manager for status display with spinner"""
with console.status(f"[bold cyan]{message}...", spinner=spinner):
yield
@staticmethod
def confirm(prompt: str, default: bool = True) -> bool:
"""Display a confirmation prompt"""
default_str = "Y/n" if default else "y/N"
response = console.input(f"[bold cyan]?[/bold cyan] {prompt} [{default_str}]: ").strip().lower()
if not response:
return default
return response in ("y", "yes")
@staticmethod
def prompt(message: str, default: Optional[str] = None) -> str:
"""Display an input prompt with optional default"""
if default:
message = f"{message} [{default}]"
response = console.input(f"[bold cyan]?[/bold cyan] {message}: ").strip()
return response or default or ""
@staticmethod
def print_command(command: str, description: Optional[str] = None):
"""Print a command with optional description"""
console.print(f"[{UIColors.CODE}]$ {command}[/{UIColors.CODE}]")
if description:
console.print(f"[dim] {description}[/dim]")
class ProgressTracker:
"""Enhanced progress tracking with context"""
def __init__(self, total_steps: int, description: str = "Setup Progress"):
self.progress = Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(bar_width=40),
TaskProgressColumn(),
TimeRemainingColumn(),
console=console,
)
self.task = None
self.total_steps = total_steps
self.description = description
self.current_step = 0
def __enter__(self):
self.progress.start()
self.task = self.progress.add_task(self.description, total=self.total_steps)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.progress.stop()
def update(self, advance: int = 1, description: Optional[str] = None):
"""Update progress with optional description"""
if description:
self.progress.update(self.task, advance=advance, description=description)
else:
self.progress.update(self.task, advance=advance)
self.current_step += advance
def set_description(self, description: str):
"""Update the progress description"""
self.progress.update(self.task, description=description)
@@ -1,118 +0,0 @@
"""
Deployment file generator for djinit.
Handles creation of Justfile, Procfile, and deployment-related files.
"""
from djinit.scripts.template_engine import template_engine
from djinit.utils import change_cwd, create_file_with_content
class DeploymentConfigGenerator:
def __init__(self, project_root: str, project_name: str):
self.project_root = project_root
self.project_name = project_name
def create_justfile(self) -> bool:
with change_cwd(self.project_root):
context = {"project_name": self.project_name}
justfile_content = template_engine.render_template("justfile.j2", context)
create_file_with_content(
"justfile",
justfile_content,
"Created justfile with Django development tasks",
)
return True
def create_procfile(self) -> bool:
with change_cwd(self.project_root):
context = {"project_name": self.project_name}
procfile_content = template_engine.render_template("procfile.j2", context)
create_file_with_content(
"Procfile",
procfile_content,
"Created Procfile with Gunicorn configuration",
)
return True
def create_gunicorn_config(self) -> bool:
with change_cwd(self.project_root):
gunicorn_config_content = f"""# Gunicorn configuration file for {self.project_name}
# Usage: gunicorn -c gunicorn_config.py {self.project_name}.wsgi:application
import multiprocessing
# Server socket
bind = "0.0.0.0:8000"
backlog = 2048
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 120
keepalive = 5
# Logging
accesslog = "-"
errorlog = "-"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
# Process naming
proc_name = "{self.project_name}"
# Server mechanics
daemon = False
pidfile = None
umask = 0
user = None
group = None
tmp_upload_dir = None
# SSL (uncomment and configure if needed)
# keyfile = "/path/to/key.pem"
# certfile = "/path/to/cert.pem"
"""
create_file_with_content(
"gunicorn_config.py",
gunicorn_config_content,
"Created gunicorn_config.py with Gunicorn configuration",
)
return True
def create_runtime_txt(self, python_version: str = "3.13") -> bool:
with change_cwd(self.project_root):
context = {"python_version": python_version}
runtime_content = template_engine.render_template("runtime_txt.j2", context)
create_file_with_content(
"runtime.txt",
runtime_content,
"Created runtime.txt with Python version specification",
)
return True
def create_nixpacks_toml(self) -> bool:
with change_cwd(self.project_root):
nixpacks_content = f"""# Nixpacks configuration for {self.project_name}
# Used by Railway, Render, and other platforms that support Nixpacks
[phases.setup]
nixPkgs = ["python311"]
[phases.install]
cmds = ["pip install --upgrade pip", "pip install -r requirements.txt"]
[phases.build]
cmds = ["python manage.py collectstatic --noinput"]
[start]
cmd = "gunicorn {self.project_name}.wsgi:application --bind 0.0.0.0:$PORT"
"""
create_file_with_content(
"nixpacks.toml",
nixpacks_content,
"Created nixpacks.toml for Nixpacks deployment",
)
return True
-163
View File
@@ -1,163 +0,0 @@
"""
File management utilities for djinit.
Handles creation and modification of project files.
"""
import os
from djinit.scripts.template_engine import template_engine
from djinit.utils import change_cwd, create_file_with_content
class FileManager:
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
self.project_root = project_root
self.project_name = project_name
self.app_names = app_names
self.metadata = metadata
self.project_configs = os.path.join(project_root, project_name)
self.settings_folder = os.path.join(self.project_configs, "settings")
self.settings_file = os.path.join(self.project_configs, "settings.py")
def create_gitignore(self) -> bool:
with change_cwd(self.project_root):
gitignore_content = template_engine.render_template("gitignore.j2", {})
return create_file_with_content(
".gitignore",
gitignore_content,
"Created .gitignore file",
)
def create_requirements(self) -> bool:
with change_cwd(self.project_root):
context = {"use_database_url": self.metadata.get("use_database_url", True)}
requirements_content = template_engine.render_template("requirements.j2", context)
return create_file_with_content(
"requirements.txt",
requirements_content,
"Created requirements.txt with Django dependencies",
)
def create_readme(self) -> bool:
with change_cwd(self.project_root):
context = {"project_name": self.project_name, "app_names": self.app_names}
readme_content = template_engine.render_template("readme.j2", context)
return create_file_with_content(
"README.md",
readme_content,
"Created README.md file",
)
def create_env_file(self) -> bool:
"""Create .env.sample file with environment variables."""
with change_cwd(self.project_root):
context = {
"project_name": self.project_name,
"use_database_url": self.metadata.get("use_database_url", True),
}
env_content = template_engine.render_template("env_sample.j2", context)
return create_file_with_content(
".env.sample",
env_content,
"Created .env.sample file with generated secret key",
)
def create_pyproject_toml(self, metadata: dict) -> bool:
with change_cwd(self.project_root):
context = {"package_name": metadata["package_name"], "project_name": self.project_name}
pyproject_content = template_engine.render_template("pyproject_toml.j2", context)
return create_file_with_content(
"pyproject.toml",
pyproject_content,
"Created pyproject.toml file for uv",
)
def _get_apps_base_dir(self) -> str:
"""Get the base directory for apps based on project structure."""
if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"):
return os.path.join(self.project_root, self.metadata.get("nested_dir"))
return self.project_root
def _create_app_file(self, apps_base_dir: str, app_name: str, template_name: str, filename: str) -> None:
"""Helper method to create app files."""
app_path = os.path.join(apps_base_dir, app_name)
with change_cwd(app_path):
context = {"app_name": app_name}
content = template_engine.render_template(template_name, context)
create_file_with_content(
filename,
content,
f"Created {app_name}/{filename}",
should_format=True,
)
def create_app_urls(self) -> bool:
apps_base_dir = self._get_apps_base_dir()
for app_name in self.app_names:
self._create_app_file(apps_base_dir, app_name, "app_urls.j2", "urls.py")
return True
def create_app_serializers(self) -> bool:
"""Create serializers.py file for each app."""
apps_base_dir = self._get_apps_base_dir()
for app_name in self.app_names:
self._create_app_file(apps_base_dir, app_name, "serializers.j2", "serializers.py")
return True
def create_app_routes(self) -> bool:
"""Create routes.py file for each app."""
apps_base_dir = self._get_apps_base_dir()
for app_name in self.app_names:
self._create_app_file(apps_base_dir, app_name, "routes.j2", "routes.py")
return True
def update_project_urls(self) -> bool:
with change_cwd(self.project_configs):
nested = bool(self.metadata.get("nested_apps"))
nested_dir = self.metadata.get("nested_dir")
effective_app_modules = [
f"{nested_dir}.{name}" if nested and nested_dir else name for name in self.app_names
]
context = {
"project_name": self.project_name,
"app_names": effective_app_modules,
}
urls_content = template_engine.render_template("project_urls.j2", context)
result = create_file_with_content(
"urls.py",
urls_content,
"Updated project urls.py with comprehensive URL configuration",
should_format=True,
)
return result
def update_wsgi_file(self) -> bool:
with change_cwd(self.project_configs):
wsgi_content = template_engine.render_template("wsgi.j2", {})
return create_file_with_content(
"wsgi.py",
wsgi_content,
"Updated wsgi.py with custom configuration",
should_format=True,
)
def update_asgi_file(self) -> bool:
with change_cwd(self.project_configs):
asgi_content = template_engine.render_template("asgi.j2", {})
return create_file_with_content(
"asgi.py",
asgi_content,
"Updated asgi.py with custom configuration",
should_format=True,
)
def update_manage_py(self) -> bool:
"""Update manage.py with custom configuration."""
with change_cwd(self.project_root):
manage_content = template_engine.render_template("manage_py.j2", {})
return create_file_with_content(
"manage.py",
manage_content,
"Updated manage.py with custom configuration",
should_format=True,
)
-406
View File
@@ -1,406 +0,0 @@
"""
User input handling for Django project setup.
Handles collection and processing of user inputs during setup.
"""
import os
import sys
import termios
import tty
from dataclasses import dataclass
from enum import Enum
from typing import Callable, Tuple
from djinit.scripts.console_ui import UIColors, UIFormatter, console
from djinit.scripts.name_validator import validate_app_name, validate_project_name
class CICDOption(Enum):
GITHUB = ("g", "GitHub Actions only")
GITLAB = ("l", "GitLab CI only")
BOTH = ("b", "Both (GitHub Actions + GitLab CI)")
NONE = ("n", "None (skip CI/CD)")
def __init__(self, key: str, description: str):
self.key = key
self.description = description
@dataclass
class ProjectMetadata:
package_name: str
use_github_actions: bool = False
use_gitlab_ci: bool = False
nested_apps: bool = False
nested_dir: str | None = None
use_database_url: bool = False
def to_dict(self) -> dict:
"""Convert to dictionary format."""
return {
"package_name": self.package_name,
"use_github_actions": self.use_github_actions,
"use_gitlab_ci": self.use_gitlab_ci,
"nested_apps": self.nested_apps,
"nested_dir": self.nested_dir,
"use_database_url": self.use_database_url,
}
@dataclass
class ProjectSetup:
"""Complete project setup configuration."""
project_dir: str
project_name: str
primary_app: str
app_names: list[str]
metadata: ProjectMetadata
def to_tuple(self) -> Tuple[str, str, str, list, dict]:
"""Convert to legacy tuple format for backward compatibility."""
return (self.project_dir, self.project_name, self.primary_app, self.app_names, self.metadata.to_dict())
class InputCollector:
"""Handles collection of user inputs with validation."""
MAX_ATTEMPTS = 3
def __init__(self):
self.char_reader = CharReader()
def get_validated_input(
self, prompt: str, validator: Callable[[str], Tuple[bool, str]], input_type: str, allow_empty: bool = False
) -> str:
attempt = 0
while attempt < self.MAX_ATTEMPTS:
try:
user_input = console.input(f"[{UIColors.HIGHLIGHT}]{prompt}:[/{UIColors.HIGHLIGHT}] ")
if not user_input.strip():
if allow_empty:
return ""
UIFormatter.print_error(f"{input_type.capitalize()} cannot be empty")
attempt += 1
self._show_retry_message(attempt)
continue
is_valid, error_msg = validator(user_input)
if is_valid:
return user_input.strip()
UIFormatter.print_error(error_msg)
attempt += 1
self._show_retry_message(attempt)
except KeyboardInterrupt:
UIFormatter.print_info("\nSetup cancelled by user.")
sys.exit(0)
except Exception as e:
UIFormatter.print_error(f"Unexpected error: {str(e)}")
attempt += 1
self._show_retry_message(attempt)
UIFormatter.print_error(f"Maximum attempts reached for {input_type}. Exiting.")
sys.exit(1)
def _show_retry_message(self, attempt: int) -> None:
if attempt < self.MAX_ATTEMPTS:
console.print(f"[{UIColors.MUTED}]Please try again ({attempt}/{self.MAX_ATTEMPTS}).[/{UIColors.MUTED}]")
def get_app_names(self) -> list[str]:
console.print(f"[{UIColors.HIGHLIGHT}]App Names[/{UIColors.HIGHLIGHT}]")
console.print(
f"[{UIColors.MUTED}]Enter app names separated by commas (no interactive prompts)[/{UIColors.MUTED}]"
)
console.print(f"[{UIColors.MUTED}]Example: users, products, orders[/{UIColors.MUTED}]")
user_input = console.input(
f"[{UIColors.HIGHLIGHT}]Enter app names (comma-separated or single):[/{UIColors.HIGHLIGHT}] "
)
# Empty input - re-prompt (no interactive flow)
if not user_input.strip():
UIFormatter.print_error("At least one app name is required")
return self.get_app_names()
# Comma-separated input
if "," in user_input:
return self._parse_comma_separated_apps(user_input)
# Single app (no additional prompts)
return self._get_apps_starting_with(user_input.strip())
def _parse_comma_separated_apps(self, user_input: str) -> list[str]:
app_list = [app.strip() for app in user_input.split(",")]
app_list = [app for app in app_list if app] # Remove empty strings
if not app_list:
UIFormatter.print_error("At least one app name is required")
return self.get_app_names()
invalid_apps = []
for app in app_list:
is_valid, error_msg = validate_app_name(app)
if not is_valid:
invalid_apps.append((app, error_msg))
if invalid_apps:
for app, error_msg in invalid_apps:
UIFormatter.print_error(f"Invalid app name '{app}': {error_msg}")
return self.get_app_names()
return app_list
def _get_apps_starting_with(self, first_app: str) -> list[str]:
is_valid, error_msg = validate_app_name(first_app)
if not is_valid:
UIFormatter.print_error(error_msg)
return self.get_app_names()
return [first_app]
def _get_apps_interactive(self) -> list[str]:
"""Deprecated: interactive flow disabled. Kept for compatibility."""
UIFormatter.print_warning("Interactive app addition is disabled. Please enter names comma-separated.")
return self.get_app_names()
def _prompt_for_additional_apps(self) -> list[str]:
"""Deprecated: interactive flow disabled. Always returns empty list."""
return []
def get_cicd_choice(self) -> Tuple[bool, bool]:
UIFormatter.print_separator()
console.print(f"\n[{UIColors.INFO}]Step 3: CI/CD Pipeline[/{UIColors.INFO}]\n")
console.print(f"[{UIColors.MUTED}]Choose CI/CD pipeline for your project[/{UIColors.MUTED}]")
console.print()
# Show options
for option in CICDOption:
console.print(f" [{UIColors.SUCCESS}]{option.key}[/{UIColors.SUCCESS}] {option.description}")
console.print()
console.print(f"[{UIColors.HIGHLIGHT}]Your choice: [/{UIColors.HIGHLIGHT}]", end="")
sys.stdout.flush()
choice = self.char_reader.get_char()
console.print(choice.upper())
# Parse choice
if choice == CICDOption.BOTH.key:
return True, True
elif choice == CICDOption.GITHUB.key:
return True, False
elif choice == CICDOption.GITLAB.key:
return False, True
else: # Default to none for any unrecognized input
return False, False
def get_nested_apps_config(self) -> Tuple[bool, str | None]:
"""Ask whether to create nested Django apps and get directory name if yes."""
UIFormatter.print_separator()
console.print(f"\n[{UIColors.INFO}]Apps Layout[/{UIColors.INFO}]\n")
console.print(
f"[{UIColors.MUTED}]Do you want to place apps inside a package directory (e.g., 'apps/')?[/{UIColors.MUTED}]"
)
console.print(
f"[{UIColors.MUTED}]If yes, we'll create that directory as a Python package and generate apps inside it.[/{UIColors.MUTED}]"
)
console.print()
console.print(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}] ", end="")
sys.stdout.flush()
choice = self.char_reader.get_char()
console.print(choice.upper())
if choice != "y":
return False, None
# Ask for directory name
dir_name = self.get_validated_input(
"Enter directory name for apps package (e.g., apps)",
validate_project_name,
"apps package name",
)
return True, dir_name
def get_database_config_choice(self) -> bool:
"""Ask user for database configuration preference."""
UIFormatter.print_separator()
console.print(f"\n[{UIColors.INFO}]Database Configuration[/{UIColors.INFO}]\n")
console.print(f"[{UIColors.MUTED}]Choose how to configure your database in production:[/{UIColors.MUTED}]")
console.print()
console.print(f" [{UIColors.SUCCESS}]Y[/{UIColors.SUCCESS}] Use DATABASE_URL (recommended for production)")
console.print(f" [{UIColors.SUCCESS}]N[/{UIColors.SUCCESS}] Use individual database parameters")
console.print()
console.print(f"[{UIColors.MUTED}]DATABASE_URL is a single environment variable like:[/{UIColors.MUTED}]")
console.print(f"[{UIColors.MUTED}]postgres://user:password@host:port/database[/{UIColors.MUTED}]")
console.print()
console.print(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}] ", end="")
sys.stdout.flush()
choice = self.char_reader.get_char()
console.print(choice.upper())
# Default to True (use DATABASE_URL) if user presses Enter or Y
return choice.lower() != "n"
class CharReader:
"""Handles single character input across platforms."""
@staticmethod
def get_char() -> str:
if os.name == "nt":
return CharReader._get_char_windows()
return CharReader._get_char_unix()
@staticmethod
def _get_char_windows() -> str:
import msvcrt
encodings = ["utf-8", "latin1"]
for encoding in encodings:
try:
ch = msvcrt.getch()
return ch.decode(encoding).lower()
except (UnicodeDecodeError, AttributeError):
continue
# Fallback
try:
return msvcrt.getch().lower()
except Exception:
return ""
@staticmethod
def _get_char_unix() -> str:
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
return ch.lower()
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
def get_user_input() -> Tuple[str, str, str, list, dict]:
collector = InputCollector()
console.print()
# Section 1: Project Setup
UIFormatter.print_separator()
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory[/{UIColors.HIGHLIGHT}]")
console.print(f"[{UIColors.MUTED}]Where your project files will be created[/{UIColors.MUTED}]")
project_dir = collector.get_validated_input(
"Enter project directory name", validate_project_name, "project directory name"
)
# Check if directory exists and ask for alternative
while os.path.exists(project_dir):
UIFormatter.print_error(f"Directory '{project_dir}' already exists.")
UIFormatter.print_info("Please choose a different project directory name.")
project_dir = collector.get_validated_input(
"Enter project directory name", validate_project_name, "project directory name"
)
console.print()
console.print(f"[{UIColors.HIGHLIGHT}]Django Project Name[/{UIColors.HIGHLIGHT}]")
console.print(f"[{UIColors.MUTED}]Name used in 'django-admin startproject' command[/{UIColors.MUTED}]")
console.print(f"[{UIColors.MUTED}]Common names: config, core, settings, project_name[/{UIColors.MUTED}]")
project_name = collector.get_validated_input(
"Enter Django project name", validate_project_name, "Django project name"
)
console.print()
# Section 2: Django Apps
console.print()
UIFormatter.print_separator()
console.print(f"\n[{UIColors.INFO}]Step 2: Django Apps[/{UIColors.INFO}]\n")
nested, nested_dir = collector.get_nested_apps_config()
app_names = collector.get_app_names()
console.print()
# Section 3: CI/CD Pipeline
use_github, use_gitlab = collector.get_cicd_choice()
# Section 4: Database Configuration
use_database_url = collector.get_database_config_choice()
# Create metadata
metadata = ProjectMetadata(
package_name=project_dir,
use_github_actions=use_github,
use_gitlab_ci=use_gitlab,
nested_apps=nested,
nested_dir=nested_dir,
use_database_url=use_database_url,
)
console.print()
# Return in legacy format for backward compatibility
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata: dict) -> bool:
console.print()
UIFormatter.print_separator()
console.print()
console.print(f"[{UIColors.INFO}]Setup Summary[/{UIColors.INFO}]")
console.print()
# Display configuration
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory:[/{UIColors.HIGHLIGHT}] {project_dir}")
console.print(f"[{UIColors.HIGHLIGHT}]Django Project:[/{UIColors.HIGHLIGHT}] {project_name}")
console.print(f"[{UIColors.HIGHLIGHT}]Apps:[/{UIColors.HIGHLIGHT}] {', '.join(app_names)}")
console.print(f"[{UIColors.HIGHLIGHT}]Package:[/{UIColors.HIGHLIGHT}] {metadata['package_name']}")
# Show CI/CD choices
cicd_choices = _get_cicd_display(metadata)
console.print(f"[{UIColors.HIGHLIGHT}]CI/CD:[/{UIColors.HIGHLIGHT}] {cicd_choices}")
# Show database configuration
db_config = "DATABASE_URL" if metadata.get("use_database_url", True) else "Individual parameters"
console.print(f"[{UIColors.HIGHLIGHT}]Database Config:[/{UIColors.HIGHLIGHT}] {db_config}")
console.print()
UIFormatter.print_separator()
console.print()
# Get confirmation
try:
console.print(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}] ", end="")
sys.stdout.flush()
reader = CharReader()
response = reader.get_char()
console.print(response.upper())
return response == "y"
except KeyboardInterrupt:
UIFormatter.print_info("\nSetup cancelled by user.")
return False
def _get_cicd_display(metadata: dict) -> str:
cicd_choices = []
if metadata.get("use_github_actions", False):
cicd_choices.append("GitHub Actions")
if metadata.get("use_gitlab_ci", False):
cicd_choices.append("GitLab CI")
return ", ".join(cicd_choices) if cicd_choices else "None"
get_char = CharReader.get_char
_get_validated_input = InputCollector().get_validated_input
-111
View File
@@ -1,111 +0,0 @@
"""
Project management for djinit.
Handles creation of Django projects and apps.
"""
import importlib.util
import os
import subprocess
import sys
from djinit.scripts.console_ui import UIFormatter
from djinit.utils import change_cwd
class ProjectManager:
def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict):
self.project_dir = project_dir
self.project_name = project_name
self.app_names = app_names
self.metadata = metadata
self.project_root = os.path.join(os.getcwd(), project_dir)
def create_project(self) -> bool:
self._ensure_django_installed()
# Create the project directory first
os.makedirs(self.project_root, exist_ok=True)
# Create Django project inside the directory
subprocess.run(
["django-admin", "startproject", self.project_name, self.project_root],
check=True,
)
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
return True
def create_apps(self) -> bool:
# Determine base directory for apps
apps_base_dir = self.project_root
nested = bool(self.metadata.get("nested_apps"))
nested_dir_name = self.metadata.get("nested_dir") if nested else None
if nested and nested_dir_name:
apps_base_dir = os.path.join(self.project_root, nested_dir_name)
os.makedirs(apps_base_dir, exist_ok=True)
# Make it a Python package
init_file = os.path.join(apps_base_dir, "__init__.py")
if not os.path.exists(init_file):
open(init_file, "a").close()
with change_cwd(apps_base_dir):
# Create each app
for app_name in self.app_names:
subprocess.run(
[
sys.executable,
os.path.join(self.project_root, "manage.py"),
"startapp",
app_name,
],
check=True,
)
UIFormatter.print_success(f"Django app '{app_name}' created successfully!")
return True
def _ensure_django_installed(self) -> None:
if importlib.util.find_spec("django") is None:
subprocess.run(
[sys.executable, "-m", "pip", "install", "--upgrade", "django", "-q"],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def validate_project_structure(self) -> bool:
required_files = [
os.path.join(self.project_root, "manage.py"),
os.path.join(self.project_root, self.project_name, "__init__.py"),
os.path.join(self.project_root, self.project_name, "settings.py"),
os.path.join(self.project_root, self.project_name, "urls.py"),
os.path.join(self.project_root, self.project_name, "wsgi.py"),
os.path.join(self.project_root, self.project_name, "asgi.py"),
]
# Add required files for each app
apps_base_dir = self.project_root
if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"):
apps_base_dir = os.path.join(self.project_root, self.metadata.get("nested_dir"))
for app_name in self.app_names:
app_files = [
os.path.join(apps_base_dir, app_name, "__init__.py"),
os.path.join(apps_base_dir, app_name, "apps.py"),
os.path.join(apps_base_dir, app_name, "models.py"),
os.path.join(apps_base_dir, app_name, "views.py"),
os.path.join(apps_base_dir, app_name, "admin.py"),
]
required_files.extend(app_files)
missing_files = [file_path for file_path in required_files if not os.path.exists(file_path)]
if not missing_files:
UIFormatter.print_success("Project structure validation passed")
return True
UIFormatter.print_error("Project structure validation failed:")
for file_path in missing_files:
UIFormatter.print_error(f" Missing: {file_path}")
return False
-86
View File
@@ -1,86 +0,0 @@
"""
Settings management for djinit.
Handles creation and configuration of Django settings files.
"""
import os
from djinit.scripts.console_ui import UIFormatter
from djinit.scripts.secretkey_generator import generate_secret_key
from djinit.scripts.template_engine import template_engine
from djinit.utils import change_cwd, create_file_with_content
class SettingsManager:
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
self.project_root = project_root
self.project_name = project_name
self.app_names = app_names
self.metadata = metadata
self.project_configs = os.path.join(project_root, project_name)
self.settings_folder = os.path.join(self.project_configs, "settings")
self.settings_file = os.path.join(self.project_configs, "settings.py")
self.secret_key = generate_secret_key(50)
def create_settings_structure(self) -> bool:
# Change to project configs directory
with change_cwd(self.project_configs):
# Create settings folder
os.makedirs("settings", exist_ok=True)
# Move settings.py to settings/base.py
if os.path.exists(self.settings_file):
os.rename(self.settings_file, os.path.join(self.settings_folder, "base.py"))
# Create __init__.py files
with change_cwd(self.settings_folder):
open("__init__.py", "a").close()
open("development.py", "a").close()
open("production.py", "a").close()
UIFormatter.print_success("Created Django settings folder structure")
return True
def update_base_settings(self) -> bool:
with change_cwd(self.settings_folder):
nested = bool(self.metadata.get("nested_apps"))
nested_dir = self.metadata.get("nested_dir")
effective_app_names = [f"{nested_dir}.{name}" if nested and nested_dir else name for name in self.app_names]
context = {
"project_name": self.project_name,
"app_names": effective_app_names,
}
base_content = template_engine.render_template("settings_base.j2", context)
create_file_with_content(
"base.py",
base_content,
"Updated settings/base.py with comprehensive configuration",
should_format=True,
)
return True
def update_development_settings(self) -> bool:
"""Update development.py with development-specific settings."""
with change_cwd(self.settings_folder):
context = {"secret_key": self.secret_key}
dev_content = template_engine.render_template("settings_development.j2", context)
create_file_with_content(
"development.py",
dev_content,
"Updated settings/development.py",
should_format=True,
)
return True
def update_production_settings(self) -> bool:
"""Update production.py with production-specific settings."""
with change_cwd(self.settings_folder):
context = {"use_database_url": self.metadata.get("use_database_url", True)}
prod_content = template_engine.render_template("settings_production.j2", context)
create_file_with_content(
"production.py",
prod_content,
"Updated settings/production.py",
should_format=True,
)
return True
View File
Whitespace-only changes.
+264
View File
@@ -0,0 +1,264 @@
"""
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
+429
View File
@@ -0,0 +1,429 @@
"""
File management utilities for djinit.
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,
)
class FileManager:
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
self.project_root = project_root
self.project_name = project_name
self.app_names = app_names
self.metadata = metadata
self.module_name = metadata.get("project_module_name") or project_name
self.project_configs = os.path.join(project_root, self.module_name)
self.settings_folder = os.path.join(self.project_configs, "settings")
self.settings_file = os.path.join(self.project_configs, "settings.py")
def _render_and_create_file(
self,
filepath: str,
template_path: str,
context: dict,
message: str,
base_dir: str = None,
should_format: bool = False,
) -> None:
"""Helper to render template and create file."""
target_dir = base_dir or self.project_root
with change_cwd(target_dir):
content = template_engine.render_template(template_path, context)
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."""
base_context = base_context or {}
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")
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}")
def _create_apps_py(self, app_dir: str, app_name: str, 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 {}
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)
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)
def _create_settings_package(self, settings_dir: str, base_context: dict, prefix: str) -> None:
"""Helper to create settings package files (base, development, production)."""
from djinit.utils.secretkey import generate_secret_key
secret_key = generate_secret_key()
dev_context = {"secret_key": secret_key}
for filename, context in [
("base.py", base_context),
("development.py", dev_context),
("production.py", base_context),
]:
filepath = os.path.join(settings_dir, filename)
template_path = f"config/settings/{filename.replace('.py', '')}.j2"
self._render_and_create_file(
filepath,
template_path,
context,
f"Created {prefix}/settings/{filename}",
base_dir=os.path.dirname(
settings_dir
), # Not strictly used by _render_and_create_file but good for context
should_format=True,
)
def _create_lifecycle_files(
self, target_dir: str, prefix: str, api_module: str = None, comment_out_api: bool = False
) -> None:
"""Helper to create urls.py, wsgi.py, and asgi.py."""
urls_context = {"api_module": api_module}
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", {}),
]
for filename, template, context in files:
filepath = os.path.join(target_dir, filename)
self._render_and_create_file(
filepath, template, context, f"Created {prefix}/{filename}", should_format=True
)
def create_gitignore(self) -> None:
self._render_and_create_file(".gitignore", "project/gitignore.j2", {}, "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"),
}
self._render_and_create_file(
"requirements.txt",
"project/requirements.j2",
context,
"Created requirements.txt with Django dependencies",
)
def create_readme(self) -> None:
context = {
"project_name": self.project_name,
"app_names": self.app_names,
"predefined_structure": bool(self.metadata.get("predefined_structure")),
"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")
def create_env_file(self) -> None:
"""Create .env.sample file with environment variables."""
context = {
"project_name": self.module_name,
"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",
context,
"Created .env.sample file with generated secret key",
)
def create_pyproject_toml(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}
self._render_and_create_file(
"pyproject.toml",
"project/pyproject_toml.j2",
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)
context = {
"project_name": self.project_name,
"app_names": effective_app_modules,
}
self._render_and_create_file(
"urls.py",
"config/urls/with_apps.j2",
context,
"Created project urls.py with comprehensive URL configuration",
base_dir=self.project_configs,
should_format=True,
)
def create_justfile(self) -> None:
context = {"project_name": self.project_name}
self._render_and_create_file(
"justfile",
"project/justfile.j2",
context,
"Created justfile with Django development tasks",
)
def create_predefined_structure(self) -> None:
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",
)
core_dir = os.path.join(apps_dir, "core")
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"
)
api_dir = os.path.join(self.project_root, "api")
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")
api_v1_dir = os.path.join(api_dir, "v1")
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")
project_urls_path = os.path.join(self.project_configs, "urls.py")
create_file_from_template(
project_urls_path,
"config/urls/with_api.j2",
{"api_module": "api"},
"Updated project urls.py to include API routes",
should_format=True,
)
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")
settings_dir = os.path.join(core_dir, "settings")
create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
# Create settings files
base_context = {
"project_name": "core",
"app_names": ["apps"],
"use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"),
}
self._create_settings_package(settings_dir, base_context, "core")
# Create lifecycle files (urls, wsgi, asgi)
self._create_lifecycle_files(core_dir, "core", api_module="apps.api")
# 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")
# Create apps.py for the 'apps' app
self._create_apps_py(apps_dir, "apps", "Created apps/apps.py", should_format=True)
# Create sub-components as directories with __init__.py
components = ["admin", "models", "serializers", "tests", "urls", "views"]
self._create_subdirectories_with_init(apps_dir, components, "apps/")
# Create 'api' directory with v1
api_dir = os.path.join(apps_dir, "api")
create_directory_with_init(api_dir, "Created apps/api/__init__.py")
create_file_from_template(
os.path.join(api_dir, "urls.py"),
"presets/predefined/api/urls.j2",
{},
"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")
create_file_from_template(
os.path.join(api_v1_dir, "urls.py"),
"presets/predefined/api/v1/urls.j2",
{},
"Created apps/api/v1/urls.py",
should_format=True,
)
def create_single_structure(self) -> None:
project_dir = os.path.join(self.project_root, self.module_name)
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")
# Create settings files
base_context = {
"project_name": self.module_name,
"app_names": [self.module_name],
"use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"),
}
self._create_settings_package(settings_dir, base_context, self.module_name)
# Create lifecycle files (urls, wsgi, asgi)
self._create_lifecycle_files(
project_dir, self.module_name, api_module=f"{self.module_name}.api", comment_out_api=True
)
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(
os.path.join(project_dir, "api", "README.md"),
"components/api_readme.j2",
{"module_name": self.module_name},
f"Created {self.module_name}/api/README.md",
)
create_file_from_template(
os.path.join(project_dir, "models", "README.md"),
"components/models_readme.j2",
{"module_name": self.module_name},
f"Created {self.module_name}/models/README.md",
)
def create_procfile(self) -> None:
context = {"project_name": self.project_name}
self._render_and_create_file(
"Procfile",
"project/procfile.j2",
context,
"Created Procfile with Gunicorn configuration",
)
def create_runtime_txt(self) -> None:
context = {"python_version": "3.13"}
self._render_and_create_file(
"runtime.txt",
"project/runtime_txt.j2",
context,
"Created runtime.txt with Python version specification",
)
def create_github_actions(self) -> None:
github_dir = os.path.join(self.project_root, ".github", "workflows")
os.makedirs(github_dir, exist_ok=True)
context = {"project_name": self.project_name, "module_name": self.module_name}
workflow_file = os.path.join(github_dir, "ci.yml")
self._render_and_create_file(
workflow_file,
"project/ci/github_actions.j2",
context,
"Created Github Actions workflow (ci.yml)",
base_dir=self.project_root,
)
def create_gitlab_ci(self) -> None:
context = {"project_name": self.project_name, "module_name": self.module_name}
self._render_and_create_file(
".gitlab-ci.yml",
"project/ci/gitlab_ci.j2",
context,
"Created GitLab CI configuration (.gitlab-ci.yml)",
)
def create_djinit_config(self) -> None:
"""Create .djinit configuration file."""
import json
is_predefined = self.metadata.get("predefined_structure", False)
is_unified = self.metadata.get("unified_structure", False)
is_single = self.metadata.get("single_structure", False)
is_standard = not (is_predefined or is_unified or is_single)
config = {
"project_name": self.project_name,
"structure": {
"standard": is_standard,
"predefined": is_predefined,
"unified": is_unified,
"single": is_single,
},
"apps": {
"nested": self.metadata.get("nested_apps", False),
"nested_dir": self.metadata.get("nested_dir"),
},
"settings": {
"use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"),
},
"cicd": {
"github": self.metadata.get("use_github_actions", False),
"gitlab": self.metadata.get("use_gitlab_ci", False),
},
}
filepath = os.path.join(self.project_root, ".djinit")
create_file_with_content(filepath, json.dumps(config, indent=4), "Created .djinit configuration file")
+161
View File
@@ -0,0 +1,161 @@
"""
Project management for djinit.
Handles creation of Django projects and apps.
"""
import os
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.django import DjangoHelper
class ProjectManager:
def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict):
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.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)
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
def _get_apps_base_dir(self) -> str:
"""Get the base directory for apps, considering nested structure."""
apps_base_dir = self.project_root
if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"):
apps_base_dir = os.path.join(self.project_root, self.metadata.get("nested_dir"))
return apps_base_dir
def create_apps(self) -> None:
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")
for app_name in self.app_names:
DjangoHelper.startapp(app_name, apps_base_dir)
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)
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)
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)
existing_apps = extract_existing_apps(content)
apps_to_add = [app for app in app_module_paths if app not in existing_apps]
if not apps_to_add:
UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS")
return
updated_content = insert_apps_into_user_defined_apps(content, apps_to_add)
if not updated_content:
from djinit.utils.exceptions import ConfigError
raise ConfigError("Could not update USER_DEFINED_APPS in base.py")
with open(base_settings_path, "w") as f:
f.write(updated_content)
added_apps_str = ", ".join(apps_to_add)
UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}")
def validate_project_structure(self) -> None:
join = lambda *args: os.path.join(self.project_root, *args)
required_files = [
join("manage.py"),
join(self.module_name, "__init__.py"),
join(self.module_name, "settings", "__init__.py"),
join(self.module_name, "settings", "base.py"),
join(self.module_name, "settings", "development.py"),
join(self.module_name, "settings", "production.py"),
join(self.module_name, "urls.py"),
join(self.module_name, "wsgi.py"),
join(self.module_name, "asgi.py"),
]
apps_base_dir = self._get_apps_base_dir()
if self.metadata.get("unified_structure"):
# Validate Unified Structure
apps_dir = join("apps")
unified_files = [
os.path.join(apps_dir, "__init__.py"),
os.path.join(apps_dir, "apps.py"),
os.path.join(apps_dir, "admin", "__init__.py"),
os.path.join(apps_dir, "api", "__init__.py"),
os.path.join(apps_dir, "api", "urls.py"),
os.path.join(apps_dir, "api", "v1", "__init__.py"),
os.path.join(apps_dir, "api", "v1", "urls.py"),
os.path.join(apps_dir, "models", "__init__.py"),
os.path.join(apps_dir, "serializers", "__init__.py"),
os.path.join(apps_dir, "tests", "__init__.py"),
os.path.join(apps_dir, "urls", "__init__.py"),
os.path.join(apps_dir, "views", "__init__.py"),
]
required_files.extend(unified_files)
elif not self.metadata.get("predefined_structure") and not self.metadata.get("single_structure"):
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("models.py"),
app_path("views.py"),
app_path("serializers.py"),
app_path("routes.py"),
app_path("tests.py"),
app_path("migrations"),
app_path("admin.py"),
]
required_files.extend(app_files)
missing_files = []
for file_path in required_files:
if file_path.endswith("settings"):
if not os.path.isdir(file_path):
missing_files.append(file_path)
elif not os.path.exists(file_path):
missing_files.append(file_path)
if not missing_files:
UIFormatter.print_success("Project structure validation passed")
return
UIFormatter.print_error("Project structure validation failed:")
for file_path in missing_files:
UIFormatter.print_error(f" Missing: {file_path}")
from djinit.utils.exceptions import ConfigError
raise ConfigError("Project structure validation failed", details=f"Missing files: {missing_files}")
@@ -20,6 +20,7 @@ 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
template_dir = Path(__file__).resolve().parent.parent / "templates"
if not template_dir.exists():
@@ -76,5 +77,4 @@ class TemplateEngine:
return os.path.exists(template_path)
# Global template engine instance
template_engine = TemplateEngine()
-9
View File
@@ -1,9 +0,0 @@
import os
from dotenv import load_dotenv
from django.core.asgi import get_asgi_application
load_dotenv()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
application = get_asgi_application()
+5
View File
@@ -0,0 +1,5 @@
"""generated with djinit"""
from django.contrib import admin
# Register your models here.
+76
View File
@@ -0,0 +1,76 @@
# API Directory
This directory contains your API endpoints organized by model/resource.
## Structure
For each model in your `models/` directory, create a corresponding subdirectory here with the following files:
```
api/
├── __init__.py
├── urls.py # Main API URL router
└── <model_name>/ # e.g., user, product, order
├── __init__.py
├── views.py
├── serializers.py
└── urls.py
```
## Example
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
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
```
### 2. `api/user/views.py`
```python
from rest_framework import viewsets
from {{ module_name }}.models.user import User
from .serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
```
### 3. `api/user/urls.py`
```python
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import UserViewSet
router = DefaultRouter()
router.register(r'', UserViewSet, basename='user')
urlpatterns = [
path('', include(router.urls)),
]
```
### 4. Update `api/urls.py`
```python
from django.urls import path, include
urlpatterns = [
path('user/', include('{{ module_name }}.api.user.urls')),
# Add more API endpoints here
]
```
## Best Practices
1. **One model per subdirectory**: Keep API endpoints for each model in its own subdirectory
2. **Consistent naming**: Use lowercase, plural names for API endpoints (e.g., `users/`, `products/`)
3. **Version your API**: Consider adding versioning (e.g., `api/v1/user/`)
4. **Use ViewSets**: Leverage DRF's ViewSets for standard CRUD operations
5. **Custom actions**: Add custom actions using `@action` decorator for non-CRUD operations
+8
View File
@@ -0,0 +1,8 @@
"""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.
+5
View File
@@ -0,0 +1,5 @@
"""generated with djinit"""
from django.db import models
# Create your models here.
@@ -0,0 +1,119 @@
# Models Directory
This directory contains your Django models.
## Structure
Organize your models as individual Python files:
```
models/
├── __init__.py
├── user.py
├── product.py
└── order.py
```
## Creating a Model
### 1. Create the model file (e.g., `models/user.py`)
```python
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
"""Custom user model."""
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'users'
verbose_name = 'User'
verbose_name_plural = 'Users'
def __str__(self):
return self.username
```
### 2. Import in `models/__init__.py`
```python
from .user import User
from .product import Product
from .order import Order
__all__ = ['User', 'Product', 'Order']
```
### 3. Create corresponding API endpoints
After creating a model, create the corresponding API structure in the `api/` directory:
```
api/
└── user/
├── __init__.py
├── views.py
├── serializers.py
└── urls.py
```
See the `api/README.md` for detailed instructions on creating API endpoints.
### 4. Register in admin (optional)
Create or update `admin/__init__.py`:
```python
from django.contrib import admin
from {{ module_name }}.models import User
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = ['username', 'email', 'created_at']
search_fields = ['username', 'email']
list_filter = ['is_active', 'created_at']
```
## Best Practices
1. **One model per file**: Keep each model in its own file for better organization
2. **Use meaningful names**: Choose clear, descriptive names for models and fields
3. **Add Meta class**: Define `db_table`, `verbose_name`, and other metadata
4. **Implement __str__**: Always define a meaningful `__str__` method
5. **Use mixins**: Create reusable mixins for common fields (e.g., timestamps)
6. **Add docstrings**: Document your models with clear docstrings
## Common Model Patterns
### Timestamped Model Mixin
```python
class TimestampedModel(models.Model):
"""Abstract base class with timestamp fields."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
```
### Soft Delete Mixin
```python
class SoftDeleteModel(models.Model):
"""Abstract base class with soft delete functionality."""
is_deleted = models.BooleanField(default=False)
deleted_at = models.DateTimeField(null=True, blank=True)
class Meta:
abstract = True
def delete(self, *args, **kwargs):
self.is_deleted = True
self.deleted_at = timezone.now()
self.save()
```
@@ -1,4 +1,6 @@
"""
generated with djinit
API routes for {{ app_name }} app.
"""
@@ -1,5 +1,6 @@
"""
Serializers for {{ app_name }} app.
generated with djinit
serializers for {{ app_name }} app.
"""
from rest_framework import serializers
+5
View File
@@ -0,0 +1,5 @@
"""generated with djinit"""
from django.test import TestCase
# Create your tests here.
@@ -1,3 +1,5 @@
"""generated with djinit"""
from django.urls import path
from . import views
+5
View File
@@ -0,0 +1,5 @@
"""generated with djinit"""
from django.shortcuts import render
# Create your views here.
+11
View File
@@ -0,0 +1,11 @@
"""generated with djinit"""
import os
import environ
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"))
application = get_asgi_application()
@@ -1,4 +1,5 @@
"""
generated with djinit
Common settings shared between development and production environment
"""
@@ -6,9 +7,10 @@ import os
from datetime import timedelta
from pathlib import Path
from dotenv import load_dotenv
import environ
load_dotenv()
env = environ.Env()
environ.Env.read_env()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
@@ -148,13 +150,28 @@ SIMPLE_JWT = {
}
# CORS Settings
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
CORS_ALLOW_METHODS = [
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
]
CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
# Security Settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
@@ -1,3 +1,5 @@
"""generated with djinit"""
from .base import *
SECRET_KEY = "{{ secret_key }}"
@@ -14,6 +16,17 @@ DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"OPTIONS": {
"transaction_mode": "IMMEDIATE",
"timeout": 5, # seconds
"init_command": """
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA mmap_size = 134217728;
PRAGMA journal_size_limit = 27103364;
PRAGMA cache_size=2000;
""",
},
}
}
@@ -21,7 +34,9 @@ DATABASES = {
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# CORS settings for development
# Allow all origins in development (convenient but not secure)
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
# Disable HTTPS redirect in development
SECURE_SSL_REDIRECT = False
@@ -0,0 +1,65 @@
"""generated with djinit"""
from .base import *
import dj_database_url
DEBUG = False
SECRET_KEY = env('SECRET_KEY')
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
# Database
{% if use_database_url %}
# Using dj_database_url (recommended for production)
DATABASES = {
'default': env.db('DATABASE_URL')
}
{% else %}
# Using individual database parameters
DATABASES = {
'default': {
{% if database_type == 'mysql' %}
'ENGINE': 'django.db.backends.mysql',
'PORT': env('DB_PORT', default='3306'),
{% else %}
'ENGINE': 'django.db.backends.postgresql',
'PORT': env('DB_PORT', default='5432'),
{% endif %}
'NAME': env('DB_NAME'),
'USER': env('DB_USER'),
'PASSWORD': env('DB_PASSWORD'),
'HOST': env('DB_HOST'),
}
}
{% endif %}
# CORS settings for production
# Filter out empty strings from FRONTEND_URL
CORS_ALLOWED_ORIGINS = env.list('FRONTEND_URL', default=[])
CORS_ALLOW_CREDENTIALS = True
# Additional CORS security for production
CORS_ALLOW_PRIVATE_NETWORK = False
# Email settings
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = env('EMAIL_HOST')
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')
# Security settings for production
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Session settings
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Static files
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
+8
View File
@@ -0,0 +1,8 @@
"""generated with djinit"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
@@ -1,3 +1,5 @@
"""generated with djinit"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
@@ -17,9 +19,8 @@ urlpatterns = [
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
{% for app in app_names %} path("", include("{{ app }}.urls")),
{% endfor %}]
{% if comment_out_api_url %}# {% endif %}path("api/", include("{{ api_module }}.urls")),
]
# Don't show schema in production
if settings.DEBUG:
+31
View File
@@ -0,0 +1,31 @@
"""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")),
]
+12
View File
@@ -0,0 +1,12 @@
"""generated with djinit"""
import os
import environ
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"))
application = get_wsgi_application()
@@ -0,0 +1,7 @@
from django.urls import include, path
app_name = "api"
urlpatterns = [
path("v1/", include("api.v1.urls")),
]
@@ -0,0 +1,7 @@
from django.urls import include, path
app_name = "v1"
urlpatterns = [
path("users/", include("apps.users.urls")),
]
@@ -0,0 +1,8 @@
from django.db import models
class {{ model_class_name }}(models.Model):
"""Generated model for {{ app_name }} app."""
# Add fields here
pass
@@ -0,0 +1,9 @@
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__"
@@ -0,0 +1,10 @@
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
@@ -0,0 +1,12 @@
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() == []
@@ -0,0 +1,14 @@
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)),
]
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,9 @@
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"]
@@ -0,0 +1,10 @@
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
@@ -0,0 +1,12 @@
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() == []
@@ -0,0 +1,14 @@
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)),
]
@@ -0,0 +1,13 @@
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)
@@ -0,0 +1,5 @@
class ApplicationError(Exception):
"""Base application exception."""
def __init__(self, message: str):
super().__init__(message)
@@ -0,0 +1,14 @@
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
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,9 @@
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)
@@ -0,0 +1,8 @@
"""generated with djinit"""
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.core"
@@ -0,0 +1,4 @@
from .base import AbstractBaseModel
from .mixins import TimestampedModel
__all__ = ["AbstractBaseModel", "TimestampedModel"]
@@ -0,0 +1,13 @@
"""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
@@ -0,0 +1,13 @@
"""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
@@ -0,0 +1,3 @@
from .responses import success, error
__all__ = ["success", "error"]
@@ -0,0 +1,13 @@
"""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)
@@ -31,10 +31,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
{% raw %}- name: Set up Python ${{ matrix.python-version }}{% endraw %}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
{% raw %}python-version: ${{ matrix.python-version }}{% endraw %}
- name: Install uv
uses: astral-sh/setup-uv@v4
@@ -62,7 +62,7 @@ jobs:
- name: Run Django tests
env:
DJANGO_SETTINGS_MODULE: {{ project_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: {{ project_name }}.settings.development
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
DB_NAME: test_db
@@ -36,7 +36,7 @@ test:
stage: test
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
ALLOWED_HOSTS: localhost
script:
@@ -52,7 +52,7 @@ test:python310:
stage: test
image: python:3.10
variables:
DJANGO_SETTINGS_MODULE: {{ project_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: {{ project_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: {{ project_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: {{ project_name }}.settings.development
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
script:
@@ -5,7 +5,11 @@ ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
{% if use_database_url %}
# Database URL (recommended for production)
DATABASE_URL=postgres://user:password@host:port/database_name
{% 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=
@@ -15,9 +19,14 @@ 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=
@@ -1,3 +1,7 @@
#--------------------------------------------------#
# The following was generated by djinit: #
#--------------------------------------------------#
# Python
*.py[cod]
*$py.class
View File
Whitespace-only changes.
@@ -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:
@@ -112,7 +112,7 @@ server:
# 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 {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --workers {{ '{{' }}WORKERS{{ '}}' }}
# Start production server with gunicorn and reload on file changes
server-watch:
@@ -143,12 +143,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{{ '}}' }}
@@ -1,8 +1,12 @@
#!/usr/bin/env python
"""generated with djinit"""
"""Django's command-line utility for administrative tasks."""
import os
import sys
from dotenv import load_dotenv
import environ
def main():
@@ -21,5 +25,8 @@ def main():
if __name__ == "__main__":
load_dotenv()
env = environ.Env()
# Read .env file, if it exists
environ.Env.read_env()
main()
File renamed without changes.
@@ -7,13 +7,14 @@ requires-python = ">=3.13"
license = { text = "MIT" }
dependencies = [
"django",
"python-dotenv",
"django-environ",
"django-jazzmin",
"djangorestframework",
"djangorestframework-simplejwt",
"drf-spectacular",
"django-cors-headers",
"whitenoise",
"mysqlclient",
"psycopg2-binary",
"gunicorn",
]
+193
View File
@@ -0,0 +1,193 @@
# {{ project_name }}
A Django project created with [djinitx/djinit/dj](https://github.com/S4NKALP/djinit).
## Features
- Modern project structure with environment-specific settings
- Pre-configured REST API with JWT authentication
- Essential dependencies and utilities
- Production-ready configuration
## Setup
### Using Just (Recommended)
1. Set environment variables in `.env` file
2. Run setup (installs dependencies, runs migrations, creates superuser):
```bash
just setup
```
3. Start development server:
```bash
just dev
```
### Traditional Method
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Set environment variables in `.env` file
3. Run migrations:
```bash
python manage.py migrate
```
4. Create superuser:
```bash
python manage.py createsuperuser
```
5. Start development server:
```bash
python manage.py runserver
```
## Available Commands
Run `just` to see all available commands, including:
- `just dev` - Run development server
- `just migrate` - Run migrations
- `just makemigrations` - Create migrations
- `just createsuperuser` - Create superuser
- `just test` - Run tests
- `just lint` - Lint code
- `just format` - Format code
- `just shell` - Django shell
- `just clean` - Clean cache files
## Project Structure
```
{% if predefined_structure %}
{{ project_name }}/
├── {{ module_name }}/
│ ├── settings/
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── apps/
│ ├── users/
│ │ ├── apps.py
│ │ ├── models/
│ │ │ └── user.py
│ │ ├── serializers/
│ │ │ └── user_serializer.py
│ │ ├── services/
│ │ │ └── user_service.py
│ │ ├── views/
│ │ │ └── user_view.py
│ │ ├── urls.py
│ │ └── tests/
│ │ └── test_user_api.py
│ └── core/
│ ├── utils/
│ │ └── responses.py
│ ├── mixins/
│ │ └── timestamped_model.py
│ ├── exceptions.py
│ └── middleware/
│ └── request_logger.py
├── api/
│ ├── __init__.py
│ ├── urls.py
│ └── v1/
│ ├── __init__.py
│ └── urls.py
├── manage.py
├── requirements.txt
├── pyproject.toml
├── .env.sample
├── justfile
├── Procfile
├── runtime.txt
└── README.md
{% elif unified_structure %}
{{ project_name }}/
├── apps/
│ ├── admin/
│ │ └── __init__.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── urls.py
│ │ └── v1/
│ │ ├── __init__.py
│ │ └── urls.py
│ ├── models/
│ │ └── __init__.py
│ ├── serializers/
│ │ └── __init__.py
│ ├── tests/
│ │ └── __init__.py
│ ├── urls/
│ │ └── __init__.py
│ └── views/
│ └── __init__.py
├── core/
│ ├── asgi.py
│ ├── __init__.py
│ ├── settings/
│ │ ├── base.py
│ │ ├── development.py
│ │ ├── __init__.py
│ │ └── production.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── requirements.txt
├── pyproject.toml
├── .env.sample
├── justfile
├── Procfile
├── runtime.txt
└── README.md
{% else %}
{{ project_name }}/
├── {{ module_name }}/
│ ├── settings/
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── admin/
│ ├── api/ # API endpoints organized by model
│ │ ├── user/ # Example: if you have a User model
│ │ │ ├── views.py
│ │ │ ├── serializers.py
│ │ │ └── urls.py
│ │ └── urls.py
│ ├── models/ # Django models
│ ├── tests/
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── manage.py
├── requirements.txt
├── pyproject.toml
├── .env.sample
└── README.md
**Note:** For the single folder layout, organize your API endpoints by model name under the `api/` directory.
For example, if you have a `User` model in `models/user.py`, create:
- `api/user/views.py` - API views for User
- `api/user/serializers.py` - Serializers for User
- `api/user/urls.py` - URL patterns for User endpoints
See the README files in `api/` and `models/` directories for more details.
{% endif %}
```
## API Documentation
When running in development mode, API documentation is available at:
- Swagger UI: http://localhost:8000/docs/
- Schema: http://localhost:8000/schema/
+19
View File
@@ -0,0 +1,19 @@
#--------------------------------------------------#
# The following was generated by djinit: #
#--------------------------------------------------#
Django
django-environ
django-jazzmin
djangorestframework
djangorestframework_simplejwt
drf-spectacular
django-cors-headers
whitenoise
{% if database_type == 'mysql' %}
mysqlclient
{% else %}
psycopg[binary]
{% endif %}
gunicorn
{% if use_database_url %}dj-database-url{% endif %}
File renamed without changes.
-60
View File
@@ -1,60 +0,0 @@
# {{ project_name }}
A Django project created with django-setup.
## Features
- Modern project structure with environment-specific settings
- Pre-configured REST API with JWT authentication
- Essential dependencies and utilities
- Production-ready configuration
## Setup
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Set environment variables in `.env` file
3. Run migrations:
```bash
python manage.py migrate
```
4. Create superuser:
```bash
python manage.py createsuperuser
```
5. Start development server:
```bash
python manage.py runserver
```
## Project Structure
```
{{ project_name }}/
├── {{ project_name }}/
│ ├── settings/
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
{% for app in app_names %}├── {{ app }}/
{% endfor %}├── manage.py
├── requirements.txt
├── pyproject.toml
├── .env.sample
└── README.md
```
## API Documentation
When running in development mode, API documentation is available at:
- Swagger UI: http://localhost:8000/docs/
- Schema: http://localhost:8000/schema/
-11
View File
@@ -1,11 +0,0 @@
Django
python-dotenv
django-jazzmin>
djangorestframework
djangorestframework_simplejwt
drf-spectacular
django-cors-headers
whitenoise
psycopg2-binary
gunicorn
{% if use_database_url %}dj-database-url{% endif %}
-51
View File
@@ -1,51 +0,0 @@
from .base import *
import os
import dj_database_url
DEBUG = False
SECRET_KEY = os.getenv('SECRET_KEY')
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')
# Database
{% if use_database_url %}
# Using dj_database_url (recommended for production)
DATABASES = {
'default': dj_database_url.parse(os.getenv('DATABASE_URL'))
}
{% else %}
# Using individual database parameters
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT'),
}
}
{% endif %}
# Email settings
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.getenv('EMAIL_HOST')
EMAIL_PORT = int(os.getenv('EMAIL_PORT', 587))
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'True').lower() == 'true'
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
# Security settings for production
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Session settings
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Static files
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
-9
View File
@@ -1,9 +0,0 @@
import os
from dotenv import load_dotenv
from django.core.wsgi import get_wsgi_application
load_dotenv()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
application = get_wsgi_application()
View File
Whitespace-only changes.
+206
View File
@@ -0,0 +1,206 @@
"""
Console UI utilities for djinit.
Provides clean, minimal styling and user interface components.
"""
from contextlib import contextmanager
from typing import Any, Dict, List, Optional
import questionary
from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
from rich.table import Table
from rich.text import Text
console = Console()
class UIColors:
"""Color scheme for consistent UI styling"""
SUCCESS = "bold green"
ERROR = "bold red"
WARNING = "bold yellow"
INFO = "bold blue"
HIGHLIGHT = "bold cyan"
MUTED = "dim white"
ACCENT = "bold magenta"
CODE = "bold white"
class Icons:
"""Minimal icon set"""
SUCCESS = "✓"
ERROR = "✗"
WARNING = "⚠"
INFO = "ℹ"
ROCKET = "🚀"
TARGET = "🎯"
LINK = "🔗"
CLOCK = "⏱"
PARTY = "🎉"
class UIFormatter:
"""Utility class for consistent UI formatting"""
@staticmethod
def print_success(message: str, icon: str = Icons.SUCCESS):
"""Print success message"""
console.print(f"[{UIColors.SUCCESS}]{icon}[/{UIColors.SUCCESS}] [bold]{message}[/bold]")
@staticmethod
def print_error(message: str, icon: str = Icons.ERROR, details: Optional[str] = None):
"""Print error message with optional details"""
console.print(f"[{UIColors.ERROR}]{icon}[/{UIColors.ERROR}] [bold]{message}[/bold]")
if details:
console.print(f" [dim]{details}[/dim]")
@staticmethod
def print_warning(message: str, icon: str = Icons.WARNING):
"""Print warning message"""
console.print(f"[{UIColors.WARNING}]{icon}[/{UIColors.WARNING}] [bold]{message}[/bold]")
@staticmethod
def print_info(message: str, icon: str = ""):
"""Print info message"""
if icon:
console.print(f"[{UIColors.INFO}]{icon}[/{UIColors.INFO}] [bold]{message}[/bold]")
else:
console.print(f"[{UIColors.INFO}]{message}[/{UIColors.INFO}]")
@staticmethod
def create_live_progress(description: str = "Setup Progress", total_steps: int = 100):
"""Create a live progress display"""
progress = Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(bar_width=40),
TaskProgressColumn(),
console=console,
)
task = progress.add_task(description, total=total_steps)
return progress, task
@staticmethod
def print_header(text: str, style: str = UIColors.ACCENT):
"""Print a styled header"""
console.print(f"\n[{style}]{'═' * 70}[/{style}]")
console.print(f"[{style}]{text.center(70)}[/{style}]")
console.print(f"[{style}]{'═' * 70}[/{style}]\n")
@staticmethod
def print_separator(char: str = "─", width: int = 70, style: str = UIColors.MUTED):
"""Print a visual separator"""
console.print(f"[{style}]{char * width}[/{style}]")
@staticmethod
def print_panel(content: str, title: str = "", border_style: str = "blue"):
"""Print content in a styled panel"""
panel = Panel(content, title=title, border_style=border_style, box=box.ROUNDED, padding=(1, 2))
console.print(panel)
@staticmethod
def print_table(data: List[Dict[str, Any]], title: Optional[str] = None):
"""Print data in a styled table"""
if not data:
return
table = Table(title=title, box=box.ROUNDED, show_header=True, header_style="bold cyan")
for key in data[0].keys():
table.add_column(key.replace("_", " ").title(), style="white")
for row in data:
table.add_row(*[str(v) for v in row.values()])
console.print(table)
@staticmethod
def create_welcome_panel():
"""Create a minimal welcome panel"""
welcome_text = Text()
welcome_text.append("\n ╔═══════════════════════════════════════╗\n", style=UIColors.ACCENT)
welcome_text.append(" ║ Django Init (djinit) ║\n", style=UIColors.ACCENT)
welcome_text.append(" ╚═══════════════════════════════════════╝\n\n", style=UIColors.ACCENT)
welcome_text.append(" Create production-ready Django projects\n\n", style=UIColors.MUTED)
welcome_text.append(" Repository: ", style=UIColors.MUTED)
welcome_text.append("https://github.com/S4NKALP/djinit\n", style="blue underline")
return welcome_text
@staticmethod
def create_summary_panel(
project_dir: str,
project_name: str,
app_names: List[str],
success: bool,
duration: Optional[float] = None,
metadata: Optional[Dict[str, Any]] = None,
):
"""Create a minimal completion summary"""
if success:
console.print("\n")
console.print(f"[bold green]{'═' * 70}[/bold green]")
console.print(f"[bold green]{f'{Icons.PARTY} SETUP COMPLETE'.center(70)}[/bold green]")
console.print(f"[bold green]{'═' * 70}[/bold green]\n")
else:
console.print("\n")
console.print(f"[bold red]{'═' * 70}[/bold red]")
console.print(f"[bold red]{'❌ SETUP FAILED'.center(70)}[/bold red]")
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
console.print("[red]Setup encountered an error. Check messages above.[/red]\n")
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
@staticmethod
@contextmanager
def status(message: str, spinner: str = "dots"):
"""Context manager for status display"""
with console.status(f"[bold cyan]{message}...", spinner=spinner):
yield
@staticmethod
def confirm(prompt: str, default: bool = True) -> bool:
"""Display a confirmation prompt as a selectable list"""
choices = [
questionary.Choice("Yes", value=True),
questionary.Choice("No", value=False),
]
default_choice = choices[0] if default else choices[1]
result = questionary.select(
prompt,
choices=choices,
default=default_choice,
).ask()
if result is None:
raise KeyboardInterrupt
return result
@staticmethod
def prompt(message: str, default: Optional[str] = None) -> str:
"""Display an input prompt"""
return questionary.text(message, default=default or "").ask()
@staticmethod
def handle_exception(e: Exception):
"""Handle and display exceptions gracefully"""
from djinit.utils.exceptions import DjinitError
if isinstance(e, DjinitError):
UIFormatter.print_error(e.message, details=e.details)
else:
console.print("\n")
console.print(f"[bold red]{'═' * 70}[/bold red]")
console.print(f"[bold red]{'💥 UNEXPECTED ERROR'.center(70)}[/bold red]")
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
console.print(f"[red]An unexpected error occurred: {str(e)}[/red]")
console.print("[dim]Please report this issue on GitHub.[/dim]\n")
# Optionally print traceback for debugging if needed, or log it
# console.print_exception()
+337
View File
@@ -0,0 +1,337 @@
"""
User input handling for Django project setup.
Handles collection and processing of user inputs during setup.
"""
import os
import sys
from typing import Tuple, TypedDict
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.validators import validate_app_name, validate_project_name
class StructureOptions(TypedDict):
project_dir: str
predefined: bool
unified: bool
single: bool
single_module_name: str | None
database_type: str
use_database_url: bool
use_github: bool
use_gitlab: bool
class InputCollector:
def __init__(self):
pass
def get_validated_input(self, prompt: str, validator: callable, input_type: str, allow_empty: bool = False) -> str:
def q_validator(text):
if not text and allow_empty:
return True
if not text:
return f"{input_type.capitalize()} cannot be empty"
is_valid, error_msg = validator(text)
if is_valid:
return True
return error_msg
result = questionary.text(prompt, validate=q_validator).ask()
if result is None:
raise KeyboardInterrupt
return result
def get_app_names(self) -> list[str]:
user_input = questionary.text(
"Enter app names (comma-separated or single, e.g. users, products, orders):",
validate=lambda text: True if text.strip() else "At least one app name is required",
).ask()
if user_input is None:
raise KeyboardInterrupt
if "," in user_input:
return self._parse_comma_separated_apps(user_input)
return self._get_apps_starting_with(user_input.strip())
def _parse_comma_separated_apps(self, user_input: str) -> list[str]:
app_list = [app.strip() for app in user_input.split(",") if app.strip()]
if not app_list:
UIFormatter.print_error("At least one app name is required")
return self.get_app_names()
return self._validate_app_list(app_list)
def _get_apps_starting_with(self, first_app: str) -> list[str]:
return self._validate_app_list([first_app])
def _validate_app_list(self, app_list: list[str]) -> list[str]:
invalid_apps = []
for app in app_list:
is_valid, error_msg = validate_app_name(app)
if not is_valid:
invalid_apps.append((app, error_msg))
if invalid_apps:
for app, error_msg in invalid_apps:
UIFormatter.print_error(f"Invalid app name '{app}': {error_msg}")
return self.get_app_names()
return app_list
def _get_project_directory(self) -> str | None:
def validate_dir(text):
if not text or text == ".":
return True
is_valid, error_msg = validate_project_name(text)
if not is_valid:
return error_msg
if os.path.exists(text):
return f"Directory '{text}' already exists."
return True
project_dir = questionary.text("Enter project directory name:", validate=validate_dir).ask()
if project_dir is None:
raise KeyboardInterrupt
if not project_dir or project_dir == ".":
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
return "."
return project_dir
def _get_selection(self, message: str, choices: list[tuple[str, str]], default: str = None) -> str:
q_choices = [questionary.Choice(title, value=value) for title, value in choices]
default_choice = next((c for c in q_choices if c.value == default), q_choices[0])
result = questionary.select(
message,
choices=q_choices,
default=default_choice,
).ask()
if result is None:
raise KeyboardInterrupt
return result
def get_cicd_choice(self) -> Tuple[bool, bool]:
choices = [
("None (skip CI/CD)", "none"),
("GitHub Actions only", "github"),
("GitLab CI only", "gitlab"),
("Both (GitHub Actions + GitLab CI)", "both"),
]
choice = self._get_selection("Select CI/CD pipeline:", choices, default="none")
choice_map = {
"both": (True, True),
"github": (True, False),
"gitlab": (False, True),
"none": (False, False),
}
return choice_map.get(choice, (False, False))
def get_nested_apps_config(self) -> Tuple[bool, str | None]:
choice = UIFormatter.confirm(
"Do you want to place apps inside a package directory (e.g. 'src/')?", default=False
)
if not choice:
return False, None
dir_name = self.get_validated_input(
"Enter directory name for apps package (e.g. src)",
validate_project_name,
"apps package name",
)
return True, dir_name
def get_database_config_choice(self) -> bool:
return UIFormatter.confirm("Use DATABASE_URL? (recommended for production)", default=True)
def get_database_type_choice(self) -> str:
choices = [
("PostgreSQL", "postgresql"),
("MySQL", "mysql"),
]
return self._get_selection("Choose database:", choices, default="postgresql")
def _get_structure_metadata(self, options: StructureOptions) -> Tuple[str, str, list[str], dict]:
"""Helper method to generate metadata dictionary."""
project_dir = options["project_dir"]
project_name = project_dir
app_names: list[str] = []
# Default package_name to "backend" if project_dir is "." or empty
package_name = get_package_name(project_dir)
project_module_name = "config" if options["predefined"] else "core" if options["unified"] else None
if options["single"]:
project_module_name = options["single_module_name"] or "project"
metadata = ProjectMetadata(
package_name=package_name,
use_github_actions=options["use_github"],
use_gitlab_ci=options["use_gitlab"],
nested_apps=not options["single"],
nested_dir="apps" if not options["single"] else None,
use_database_url=options["use_database_url"],
database_type=options["database_type"],
predefined_structure=options["predefined"],
unified_structure=options["unified"],
single_structure=options["single"],
project_module_name=project_module_name,
)
return project_name, "", app_names, metadata.to_dict()
def get_user_input() -> Tuple[str, str, str, list, dict]:
try:
collector = InputCollector()
structure_choices = [
("Standard structure (default Django layout)", "1"),
("Predefined structure (apps/users, apps/core, api/)", "2"),
("Unified structure (core/, apps/core, apps/api)", "3"),
("Single folder layout (everything in one folder)", "4"),
]
structure_choice = collector._get_selection("Choose structure type:", structure_choices, default="1")
use_standard = structure_choice == "1"
use_predefined = structure_choice == "2"
use_unified = structure_choice == "3"
use_single = structure_choice == "4"
# Step 1: Project Setup
project_dir = collector._get_project_directory()
if project_dir is None:
# Should be handled by questionary loop, but just in case
sys.exit(1)
project_name = project_dir
single_module_name = None
if use_standard:
console.print(f"[{UIColors.MUTED}]Common names: config, core, settings[/{UIColors.MUTED}]")
project_name = collector.get_validated_input(
"Enter Django project name", validate_project_name, "Django project name"
)
elif use_single:
console.print(f"[{UIColors.MUTED}]Common names: project, core, app[/{UIColors.MUTED}]")
single_module_name = (
collector.get_validated_input(
"Enter project configuration directory name (default: project)",
validate_project_name,
"directory name",
allow_empty=True,
)
or "project"
)
# Step 2: Database Configuration
database_type = collector.get_database_type_choice()
use_database_url = collector.get_database_config_choice()
# Step 3: Django Apps (Standard only)
nested = False
nested_dir = None
app_names = []
if use_standard:
nested, nested_dir = collector.get_nested_apps_config()
app_names = collector.get_app_names()
# Step 3/4: CI/CD Pipeline
use_github, use_gitlab = collector.get_cicd_choice()
# Generate Metadata
if use_standard:
package_name = get_package_name(project_dir)
metadata = ProjectMetadata(
package_name=package_name,
use_github_actions=use_github,
use_gitlab_ci=use_gitlab,
nested_apps=nested,
nested_dir=nested_dir,
use_database_url=use_database_url,
database_type=database_type,
)
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
else:
options = StructureOptions(
project_dir=project_dir,
predefined=use_predefined,
unified=use_unified,
single=use_single,
single_module_name=single_module_name,
database_type=database_type,
use_database_url=use_database_url,
use_github=use_github,
use_gitlab=use_gitlab,
)
project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata(options)
return project_dir, project_name, primary_app, app_names, metadata_dict
except KeyboardInterrupt:
UIFormatter.print_info("\nSetup cancelled by user.")
sys.exit(0)
def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata: dict) -> bool:
console.print()
UIFormatter.print_separator()
console.print()
console.print(f"[{UIColors.INFO}]Setup Summary[/{UIColors.INFO}]")
console.print()
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory:[/{UIColors.HIGHLIGHT}] {project_dir}")
console.print(f"[{UIColors.HIGHLIGHT}]Django Project:[/{UIColors.HIGHLIGHT}] {project_name}")
console.print(f"[{UIColors.HIGHLIGHT}]Apps:[/{UIColors.HIGHLIGHT}] {', '.join(app_names)}")
console.print(f"[{UIColors.HIGHLIGHT}]Package:[/{UIColors.HIGHLIGHT}] {metadata['package_name']}")
cicd_choices = _get_cicd_display(metadata)
console.print(f"[{UIColors.HIGHLIGHT}]CI/CD:[/{UIColors.HIGHLIGHT}] {cicd_choices}")
db_config = "DATABASE_URL" if metadata.get("use_database_url", True) else "Individual parameters"
console.print(f"[{UIColors.HIGHLIGHT}]Database Config:[/{UIColors.HIGHLIGHT}] {db_config}")
db_type = metadata.get("database_type", "postgresql").capitalize()
console.print(f"[{UIColors.HIGHLIGHT}]Database Type:[/{UIColors.HIGHLIGHT}] {db_type}")
console.print()
UIFormatter.print_separator()
console.print()
try:
return UIFormatter.confirm("Proceed with setup?", default=True)
except KeyboardInterrupt:
UIFormatter.print_info("\nSetup cancelled by user.")
return False
def _get_cicd_display(metadata: dict) -> str:
choices = []
if metadata.get("use_github_actions", False):
choices.append("GitHub Actions")
if metadata.get("use_gitlab_ci", False):
choices.append("GitLab CI")
return ", ".join(choices) if choices else "None"
-52
View File
@@ -1,52 +0,0 @@
"""
Shared utility functions.
Contains common operations used across multiple managers.
"""
import os
import subprocess
from contextlib import contextmanager
from djinit.scripts.console_ui import UIFormatter
def format_file(filename: str) -> None:
"""Format Python file using Ruff formatter."""
subprocess.run(["ruff", "format", filename], check=False, capture_output=True)
def create_file_with_content(filename: str, content: str, success_message: str, should_format: bool = False) -> bool:
"""
Create a file with content, optionally format it, and print success message.
Args:
filename: Name of the file to create
content: Content to write to the file
success_message: Message to print on success
should_format: Whether to format the file with ruff
Returns:
True if successful
"""
with open(filename, "w") as file:
file.write(content)
if should_format:
format_file(filename)
UIFormatter.print_success(success_message)
return True
@contextmanager
def change_cwd(path: str):
"""Temporarily change the current working directory.
Ensures the original directory is restored even if an exception occurs.
"""
original_cwd = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(original_cwd)
View File
Whitespace-only changes.
+302
View File
@@ -0,0 +1,302 @@
"""
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
+89
View File
@@ -0,0 +1,89 @@
"""
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
+30
View File
@@ -0,0 +1,30 @@
"""
Custom exceptions for djinit.
"""
class DjinitError(Exception):
"""Base exception for all djinit errors."""
def __init__(self, message: str, details: str = None):
self.message = message
self.details = details
super().__init__(message)
class ConfigError(DjinitError):
"""Raised when there is a configuration or metadata error."""
pass
class FileError(DjinitError):
"""Raised when a file operation fails."""
pass
class TemplateError(DjinitError):
"""Raised when template rendering fails."""
pass
@@ -8,11 +8,7 @@ import secrets
import string
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from djinit.scripts.console_ui import UIColors
console = Console()
@@ -38,22 +34,6 @@ def display_secret_keys(keys: list[str]) -> None:
console.print()
def generate_secret_command():
if __name__ == "__main__":
keys = generate_multiple_keys(3, 50)
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()
if __name__ == "__main__":
generate_secret_command()
@@ -5,14 +5,9 @@ Handles validation of project names, app names, and other user inputs.
import keyword
import re
import sys
from typing import Any, Callable, Tuple
# Names that conflict with Django's structure
DJANGO_RESERVED = {"django", "test", "site-packages", "admin"}
# Common Python builtin modules
PYTHON_BUILTINS = set(sys.builtin_module_names)
from djinit.core.config import PYTHON_BUILTINS
NAME_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*$")
Loaded 100 of 104 files, more files were not shown because too many files have changed in this diff. Show more