From 9d699abea3e6ce1d4b3fab401b86bb2d7f402778 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 01:38:50 +0545 Subject: [PATCH 01/24] refactor(root): drop jinja2 dependency Enhancements - Removed unused `jinja2` dependency from `pyproject.toml` and `requirements.txt`. --- pyproject.toml | 1 - requirements.txt | 1 - setup.py | 1 - uv.lock | 99 ------------------------------------------------ 4 files changed, 102 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bf11c18..2b27215 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,6 @@ classifiers=[ ] dependencies = [ "rich>=14.2.0", - "jinja2>=3.1.0", "questionary>=2.0.1", ] diff --git a/requirements.txt b/requirements.txt index 395a168..f98ad4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -jinja2==3.1.6 questionary==2.1.1 rich==14.3.1 # dev diff --git a/setup.py b/setup.py index f782abb..3127149 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,6 @@ setup( "ruff>=0.14.2", "click>=8.3.0", "rich>=14.2.0", - "jinja2>=3.1.0", "questionary>=2.0.1", ], entry_points={ diff --git a/uv.lock b/uv.lock index 129e592..8060923 100644 --- a/uv.lock +++ b/uv.lock @@ -25,7 +25,6 @@ name = "djinitx" version = "0.3.3" source = { editable = "." } dependencies = [ - { name = "jinja2" }, { name = "questionary" }, { name = "rich" }, ] @@ -38,7 +37,6 @@ dev = [ [package.metadata] requires-dist = [ - { name = "jinja2", specifier = ">=3.1.0" }, { name = "questionary", specifier = ">=2.0.1" }, { name = "rich", specifier = ">=14.2.0" }, ] @@ -67,18 +65,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, ] -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -91,91 +77,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" -- 2.43.0 From 6cc5fe43e6e691b8387da7d450fdebcb320a041b Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 01:38:56 +0545 Subject: [PATCH 02/24] feat(djinit/utils): Use .py-tpl templates for generated Django files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change updates all template references from *.j2 to *.py-tpl to align with the new file naming convention and adds the required context for URL generation. New Features - Updated template paths to *.py-tpl for init, manage.py, settings, and component files - Added `url_type` context for URL generation templates Enhancements - Modified `create_init_file` to use `init.py-tpl` template - Updated all `create_file_from_template` calls to reference `.py-tpl` templates - Adjusted context for URL templates to include project‑specific data - Switched component URL template to `config/urls.py-tpl` Bug Fixes - None --- djinit/utils/common.py | 2 +- djinit/utils/django.py | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/djinit/utils/common.py b/djinit/utils/common.py index eb9e13e..4af222c 100644 --- a/djinit/utils/common.py +++ b/djinit/utils/common.py @@ -51,7 +51,7 @@ def create_file_from_template( 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) + create_file_from_template(init_path, "project/init.py-tpl", {}, success_message, should_format=False) def create_directory_with_init(dir_path: str, message: str) -> None: diff --git a/djinit/utils/django.py b/djinit/utils/django.py index f893210..87cb233 100644 --- a/djinit/utils/django.py +++ b/djinit/utils/django.py @@ -25,7 +25,7 @@ class DjangoHelper: manage_py_path = os.path.join(directory, "manage.py") create_file_from_template( - manage_py_path, "project/manage_py.j2", {"project_name": project_name}, "Created manage.py" + manage_py_path, "project/manage.py-tpl", {"project_name": project_name}, "Created manage.py" ) os.chmod(manage_py_path, 0o755) @@ -40,17 +40,17 @@ class DjangoHelper: 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", {}), + ("base.py", "config/settings/base.py-tpl", base_context), + ("development.py", "config/settings/development.py-tpl", {}), + ("production.py", "config/settings/production.py-tpl", {}), ] create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/") - urls_context = {"project_name": project_name, "django_version": DjangoHelper.DJANGO_VERSION} + urls_context = {"url_type": "project", "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", {}), + ("urls.py", "config/urls.py-tpl", urls_context), + ("wsgi.py", "config/wsgi.py-tpl", {}), + ("asgi.py", "config/asgi.py-tpl", {}), ] create_files_from_templates(project_config_dir, project_files, f"{project_name}/") @@ -70,14 +70,14 @@ class DjangoHelper: 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), + ("apps.py", "components/apps.py-tpl", context), + ("models.py", "components/models.py-tpl", context), + ("views.py", "components/views.py-tpl", context), + ("admin.py", "components/admin.py-tpl", context), + ("urls.py", "config/urls.py-tpl", {"url_type": "component"}), + ("serializers.py", "components/serializers.py-tpl", context), + ("routes.py", "components/routes.py-tpl", context), + ("tests.py", "components/tests.py-tpl", context), ] create_files_from_templates(app_dir, app_files, f"{app_name}/") -- 2.43.0 From a65e12442e7f40b5e3be58d4b663aad87d66682c Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 01:39:00 +0545 Subject: [PATCH 03/24] feat(djinit): introduce .py-tpl template convention and modernize rendering pipeline New Features - Adopted .py-tpl/.j2-tpl suffixes for generated templates, making them distinct and Python-specific. Enhancements - Refactored TemplateEngine to use InFileLogicParser instead of Jinja2, simplifying rendering logic. - Updated CLI utility to call create_pyproject for pyproject.toml generation. - Modified template discovery to scan only files ending in -tpl. - Adjusted file generation paths for Django app components and configuration files. Bug Fixes - Resolved template path resolution issues arising from the new suffix convention. --- djinit/cli.py | 2 +- djinit/services/app.py | 16 +++---- djinit/services/files.py | 93 +++++++++++++++++++----------------- djinit/services/templates.py | 32 ++++++++----- 4 files changed, 78 insertions(+), 65 deletions(-) diff --git a/djinit/cli.py b/djinit/cli.py index 0cc0641..318a58d 100644 --- a/djinit/cli.py +++ b/djinit/cli.py @@ -104,7 +104,7 @@ class Cli: 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), + lambda: self.file_manager.create_pyproject(self.metadata), ] for step_func in utility_steps: diff --git a/djinit/services/app.py b/djinit/services/app.py index bcc5636..660673c 100644 --- a/djinit/services/app.py +++ b/djinit/services/app.py @@ -196,17 +196,17 @@ class AppManager: create_file_from_template( os.path.join(app_dir, "apps.py"), - "components/apps.j2", + "components/apps.py-tpl", {"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")], + "models": [(f"{self.app_name}.py", "presets/predefined/apps/generic/models.py-tpl")], + "serializers": [(f"{self.app_name}_serializer.py", "presets/predefined/apps/generic/serializers.py-tpl")], + "services": [(f"{self.app_name}_service.py", "presets/predefined/apps/generic/services.py-tpl")], + "views": [(f"{self.app_name}_view.py", "presets/predefined/apps/generic/views.py-tpl")], + "tests": [(f"test_{self.app_name}_api.py", "presets/predefined/apps/generic/tests.py-tpl")], } for folder, files in subfolders.items(): folder_path = os.path.join(app_dir, folder) @@ -219,8 +219,8 @@ class AppManager: 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}, + "config/urls.py-tpl", + {"url_type": "drf_router", "app_module": app_module, "view_file": f"{self.app_name}_view", "viewset_name": f"{model_class_name}ViewSet", "router_prefix": "", "basename": self.app_name, "has_viewset": True, "has_include": False}, f"Created apps/{self.app_name}/urls.py", ) diff --git a/djinit/services/files.py b/djinit/services/files.py index 34f3a3d..c400af1 100644 --- a/djinit/services/files.py +++ b/djinit/services/files.py @@ -59,7 +59,7 @@ class FileManager: 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) + create_file_from_template(apps_py_path, "components/apps.py-tpl", context, msg, should_format=should_format) def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None: """Helper to create multiple subdirectories with __init__.py files.""" @@ -81,7 +81,7 @@ class FileManager: ("production.py", base_context), ]: filepath = os.path.join(settings_dir, filename) - template_path = f"config/settings/{filename.replace('.py', '')}.j2" + template_path = f"config/settings/{filename.replace('.py', '')}.py-tpl" self._render_and_create_file( filepath, template_path, @@ -98,14 +98,14 @@ class FileManager: ) -> None: """Helper to create urls.py, wsgi.py, and asgi.py.""" - urls_context = {"api_module": api_module} + urls_context = {"url_type": "project", "api_module": api_module, "include_api": True} if comment_out_api: urls_context["comment_out_api_url"] = True files = [ - ("urls.py", "config/urls/with_api.j2", urls_context), - ("wsgi.py", "config/wsgi.j2", {"project_name": prefix}), - ("asgi.py", "config/asgi.j2", {"project_name": prefix}), + ("urls.py", "config/urls.py-tpl", urls_context), + ("wsgi.py", "config/wsgi.py-tpl", {"project_name": prefix}), + ("asgi.py", "config/asgi.py-tpl", {"project_name": prefix}), ] for filename, template, context in files: @@ -115,7 +115,7 @@ class FileManager: ) def create_gitignore(self) -> None: - self._render_and_create_file(".gitignore", "project/gitignore.j2", {}, "Created .gitignore file") + self._render_and_create_file(".gitignore", "project/gitignore-tpl", {}, "Created .gitignore file") def create_requirements(self) -> None: context = { @@ -124,7 +124,7 @@ class FileManager: } self._render_and_create_file( "requirements.txt", - "project/requirements.j2", + "project/requirements-tpl", context, "Created requirements.txt with Django dependencies", ) @@ -137,7 +137,7 @@ class FileManager: "unified_structure": bool(self.metadata.get("unified_structure")), "module_name": self.module_name, } - self._render_and_create_file("README.md", "project/readme.j2", context, "Created README.md file") + self._render_and_create_file("README.md", "project/readme.md-tpl", context, "Created README.md file") def create_env_file(self) -> None: """Create .env.sample file with environment variables.""" @@ -148,32 +148,39 @@ class FileManager: } self._render_and_create_file( ".env.sample", - "project/env_sample.j2", + "project/env_sample-tpl", context, "Created .env.sample file with generated secret key", ) - def create_pyproject_toml(self, metadata: dict) -> None: + def create_pyproject(self, metadata: dict) -> None: package_name = metadata.get("package_name", "backend") package_name = get_package_name(package_name) - context = {"package_name": package_name, "project_name": self.project_name} + context = { + "package_name": package_name, + "project_name": self.project_name, + "database_type": metadata.get("database_type", "postgresql"), + } self._render_and_create_file( "pyproject.toml", - "project/pyproject_toml.j2", + "project/pyproject.toml-tpl", context, "Created pyproject.toml file for uv", ) def create_project_urls(self) -> None: - """Create project urls.py using project_urls.j2 template (replaces update_project_urls).""" + """Create project urls.py using project_config/urls.py-tpl template (replaces update_project_urls).""" effective_app_modules = calculate_app_module_paths(self.app_names, self.metadata) context = { + "url_type": "project", "project_name": self.project_name, "app_names": effective_app_modules, + "include_apps": True, + "include_api": True, } self._render_and_create_file( "urls.py", - "config/urls/with_apps.j2", + "config/urls.py-tpl", context, "Created project urls.py with comprehensive URL configuration", base_dir=self.project_configs, @@ -184,7 +191,7 @@ class FileManager: context = {"project_name": self.project_name} self._render_and_create_file( "justfile", - "project/justfile.j2", + "project/justfile-tpl", context, "Created justfile with Django development tasks", ) @@ -198,19 +205,19 @@ class FileManager: 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")], + "models": [("user.py", "presets/predefined/apps/users/models.py-tpl")], + "serializers": [("user_serializer.py", "presets/predefined/apps/users/serializers.py-tpl")], + "services": [("user_service.py", "presets/predefined/apps/users/services.py-tpl")], + "views": [("user_view.py", "presets/predefined/apps/users/views.py-tpl")], + "tests": [("test_user_api.py", "presets/predefined/apps/users/tests.py-tpl")], } 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"}, + "config/urls.py-tpl", + {"url_type": "drf_router", "app_module": "apps.users", "view_file": "user_view", "viewset_name": "UserViewSet", "router_prefix": "users", "basename": "users", "has_viewset": True, "has_app_name": True, "app_name": "users", "has_include": True}, "Created apps/users/urls.py", ) @@ -218,32 +225,32 @@ class FileManager: 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")], + "utils": [("responses.py", "presets/predefined/core/utils/responses.py-tpl")], + "mixins": [("timestamped_model.py", "presets/predefined/core/mixins/timestamped_model.py-tpl")], + "middleware": [("request_logger.py", "presets/predefined/core/middleware/request_logger.py-tpl")], } 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" + exceptions_path, "presets/predefined/core/exceptions.py-tpl", {}, "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") + create_file_from_template(api_urls_path, "config/urls.py-tpl", {"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "api"}, "Created api/urls.py") api_v1_dir = os.path.join(api_dir, "v1") create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py") 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") + create_file_from_template(api_v1_urls_path, "config/urls.py-tpl", {"url_type": "api_version", "app_name": "v1", "app_list": ["users"], "app_module": "apps"}, "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"}, + "config/urls.py-tpl", + {"url_type": "project", "api_module": "api", "include_api": True}, "Updated project urls.py to include API routes", should_format=True, ) @@ -285,8 +292,8 @@ class FileManager: create_file_from_template( os.path.join(api_dir, "urls.py"), - "presets/predefined/api/urls.j2", - {}, + "config/urls.py-tpl", + {"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "apps.api"}, "Created apps/api/urls.py", should_format=True, ) @@ -296,8 +303,8 @@ class FileManager: create_file_from_template( os.path.join(api_v1_dir, "urls.py"), - "presets/predefined/api/v1/urls.j2", - {}, + "config/urls.py-tpl", + {"url_type": "api_version", "app_name": "v1", "app_list": [], "app_module": "apps"}, "Created apps/api/v1/urls.py", should_format=True, ) @@ -328,28 +335,28 @@ class FileManager: create_file_from_template( os.path.join(project_dir, "admin", "__init__.py"), - "components/admin.j2", + "components/admin.py-tpl", {}, f"Created {self.module_name}/admin/__init__.py", ) create_file_from_template( os.path.join(project_dir, "models", "__init__.py"), - "components/models.j2", + "components/models.py-tpl", {}, f"Created {self.module_name}/models/__init__.py", ) create_file_from_template( os.path.join(project_dir, "api", "README.md"), - "components/api_readme.j2", + "components/api_readme.md-tpl", {"module_name": self.module_name}, f"Created {self.module_name}/api/README.md", ) create_file_from_template( os.path.join(project_dir, "models", "README.md"), - "components/models_readme.j2", + "components/models_readme.md-tpl", {"module_name": self.module_name}, f"Created {self.module_name}/models/README.md", ) @@ -358,7 +365,7 @@ class FileManager: context = {"project_name": self.project_name} self._render_and_create_file( "Procfile", - "project/procfile.j2", + "project/procfile-tpl", context, "Created Procfile with Gunicorn configuration", ) @@ -367,7 +374,7 @@ class FileManager: context = {"python_version": "3.13"} self._render_and_create_file( "runtime.txt", - "project/runtime_txt.j2", + "project/runtime_txt-tpl", context, "Created runtime.txt with Python version specification", ) @@ -379,7 +386,7 @@ class FileManager: workflow_file = os.path.join(github_dir, "ci.yml") self._render_and_create_file( workflow_file, - "project/ci/github_actions.j2", + "project/ci/github_actions-tpl", context, "Created Github Actions workflow (ci.yml)", base_dir=self.project_root, @@ -389,7 +396,7 @@ class FileManager: context = {"project_name": self.project_name, "module_name": self.module_name} self._render_and_create_file( ".gitlab-ci.yml", - "project/ci/gitlab_ci.j2", + "project/ci/gitlab_ci-tpl", context, "Created GitLab CI configuration (.gitlab-ci.yml)", ) diff --git a/djinit/services/templates.py b/djinit/services/templates.py index 4f74608..e7a7f0b 100644 --- a/djinit/services/templates.py +++ b/djinit/services/templates.py @@ -7,7 +7,7 @@ import os from pathlib import Path from typing import Any, Dict -from jinja2 import Environment, FileSystemLoader, Template +from djinit.core.parser import InFileLogicParser class TemplateEngine: @@ -27,22 +27,26 @@ class TemplateEngine: raise FileNotFoundError(f"Template directory not found: {template_dir}") self.template_dir = str(template_dir) - self.env = Environment( - loader=FileSystemLoader(template_dir), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True - ) + self.parser = InFileLogicParser() def render_template(self, template_name: str, context: Dict[str, Any]) -> str: """Render a template with the given context. Args: - template_name: Name of the template file (e.g., 'gitignore.j2') + template_name: Name of the template file (e.g., 'gitignore.py-tpl') context: Dictionary of variables to pass to the template Returns: Rendered template content as string """ - template = self.env.get_template(template_name) - return template.render(**context) + template_path = os.path.join(self.template_dir, template_name) + if not os.path.exists(template_path): + raise FileNotFoundError(f"Template not found: {template_path}") + + with open(template_path, "r") as f: + template_text = f.read() + + return self.parser.render(template_text, context) def render_string(self, template_string: str, context: Dict[str, Any]) -> str: """Render a template string with the given context. @@ -54,24 +58,26 @@ class TemplateEngine: Returns: Rendered template content as string """ - template = Template(template_string, trim_blocks=True, lstrip_blocks=True) - return template.render(**context) + return self.parser.render(template_string, context) def get_template_names(self) -> list[str]: """Get list of available template files.""" if not os.path.exists(self.template_dir): return [] - return sorted([file for file in os.listdir(self.template_dir) if file.endswith(".j2")]) + templates = [] + for root, _, files in os.walk(self.template_dir): + for file in files: + if file.endswith("-tpl"): + rel_path = os.path.relpath(os.path.join(root, file), self.template_dir) + templates.append(rel_path) + return sorted(templates) def template_exists(self, template_name: str) -> bool: """Check if a template file exists. Args: template_name: Name of the template file - - Returns: - True if template exists, False otherwise """ template_path = os.path.join(self.template_dir, template_name) return os.path.exists(template_path) -- 2.43.0 From bc23b0200712343c0a96d654d6a74d21396cdbac Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 01:39:04 +0545 Subject: [PATCH 04/24] feat(djinit/core): add InFileLogicParser template rendering module - New Features * Introduce InFileLogicParser with support for @IF, @ELSEIF, @ELSE, @ENDIF, @LOOP syntax - Enhancements * Enable nested conditionals and iteration over iterables within templates - Bug Fixes * None --- djinit/core/parser.py | 215 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 djinit/core/parser.py diff --git a/djinit/core/parser.py b/djinit/core/parser.py new file mode 100644 index 0000000..542ad44 --- /dev/null +++ b/djinit/core/parser.py @@ -0,0 +1,215 @@ +import re +from typing import Any, Dict, List + + +class InFileLogicParser: + """ + A lightweight template parser that uses comment-style markers for logic + and [[ variable ]] for substitutions. + """ + + def __init__(self, context: Dict[str, Any] = None): + self.context = context or {} + + def _get_value(self, key: str) -> str: + """ + Supports nested/dotted access like [[ user.name ]] or [[ settings["DEBUG"] ]]. + For simplicity, it uses eval in the provided context for complex expressions, + but falls back to safe dictionary lookup for simple keys. + """ + key = key.strip() + try: + # Try evaluating the expression in the context + return str(eval(key, {"__builtins__": {}}, self.context)) + except (NameError, SyntaxError, KeyError, TypeError, AttributeError): + # Fallback for common patterns or just return the key as is if it fails + return f"[[ {key} ]]" + + def render(self, template_text: str, context: Dict[str, Any] = None) -> str: + if context is not None: + self.context = context + + final_lines = [] + # Stack stores boolean results of nested IF blocks + # Each element is (current_block_result, has_any_true_branch_executed) + stack: List[List[bool]] = [] + + lines = template_text.splitlines() + i = 0 + while i < len(lines): + line = lines[i] + stripped = line.strip() + + # Handle @IF + if stripped.startswith("# @IF "): + expr = stripped[6:].strip() + try: + result = bool(eval(expr, {"__builtins__": {}}, self.context)) + except Exception: + result = False + stack.append([result, result]) + i += 1 + continue + + # Handle @ELSEIF + elif stripped.startswith("# @ELSEIF "): + if not stack: + final_lines.append(line) + i += 1 + continue + + expr = stripped[9:].strip() + current_stack = stack[-1] + + if current_stack[1]: + current_stack[0] = False + else: + try: + result = bool(eval(expr, {"__builtins__": {}}, self.context)) + except Exception: + result = False + current_stack[0] = result + if result: + current_stack[1] = True + i += 1 + continue + + # Handle @ELSE + elif stripped.startswith("# @ELSE"): + if not stack: + final_lines.append(line) + i += 1 + continue + + current_stack = stack[-1] + if current_stack[1]: + current_stack[0] = False + else: + current_stack[0] = True + current_stack[1] = True + i += 1 + continue + + # Handle @ENDIF + elif stripped.startswith("# @ENDIF"): + if stack: + stack.pop() + else: + final_lines.append(line) + i += 1 + continue + + # Handle @LOOP + elif stripped.startswith("# @LOOP "): + if stack and not all(s[0] for s in stack): + # Skip the entire loop block if inside a false conditional + loop_depth = 1 + i += 1 + while i < len(lines) and loop_depth > 0: + if lines[i].strip().startswith("# @LOOP "): + loop_depth += 1 + elif lines[i].strip().startswith("# @ENDLOOP"): + loop_depth -= 1 + i += 1 + continue + + parts = stripped[7:].strip().split(" in ") + if len(parts) == 2: + var_name = parts[0].strip() + iterable_name = parts[1].strip() + try: + iterable = eval(iterable_name, {"__builtins__": {}}, self.context) + except Exception: + iterable = [] + + # Capture loop body + loop_body = [] + i += 1 + loop_depth = 1 + while i < len(lines) and loop_depth > 0: + if lines[i].strip().startswith("# @LOOP "): + loop_depth += 1 + elif lines[i].strip().startswith("# @ENDLOOP"): + loop_depth -= 1 + + if loop_depth > 0: + loop_body.append(lines[i]) + i += 1 + + if i < len(lines): # Skip the @ENDLOOP line itself + i += 1 + + # Execute loop + if hasattr(iterable, "__iter__"): + old_val = self.context.get(var_name) + for val in iterable: + self.context[var_name] = val + rendered_body = self.render("\n".join(loop_body), self.context) + final_lines.extend(rendered_body.splitlines()) + if old_val is not None: + self.context[var_name] = old_val + else: + self.context.pop(var_name, None) + continue + else: + final_lines.append(line) + i += 1 + continue + + # Handle @ENDLOOP (should only be hit if out of sync or error) + elif stripped.startswith("# @ENDLOOP"): + i += 1 + continue + + # Check if we should skip this line based on conditional stack + if stack and not all(s[0] for s in stack): + i += 1 + continue + + # Variable substitution + rendered_line = line + matches = re.findall(r"\[\[\s*(.*?)\s*\]\]", rendered_line) + for match in matches: + value = self._get_value(match) + rendered_line = rendered_line.replace(f"[[ {match} ]]", value) + rendered_line = rendered_line.replace(f"[[{match}]]", value) + + # In-line @IF support + # Case 1: content # @IF cond + # Case 2: # @IF cond content + if "# @IF " in rendered_line: + parts = rendered_line.split("# @IF ", 1) + pre_content = parts[0].rstrip() + rest = parts[1].strip() + + # Split rest into expression and post-content (if any) + # This is tricky because the expression might have spaces. + # If there's an # @ENDIF on the same line, use it. + post_content = "" + expr = rest + if " # @ENDIF" in rest: + expr_parts = rest.split(" # @ENDIF", 1) + expr = expr_parts[0].strip() + post_content = expr_parts[1].lstrip() + elif "# @ENDIF" in rest: + expr_parts = rest.split("# @ENDIF", 1) + expr = expr_parts[0].strip() + post_content = expr_parts[1].lstrip() + + try: + if bool(eval(expr, {"__builtins__": {}}, self.context)): + # If Case 1, we want pre_content. If Case 2, we want post_content. + # Usually, if pre_content is empty (or just whitespace), it's Case 2. + if pre_content: + final_lines.append(pre_content + post_content) + else: + final_lines.append(post_content) + except Exception: + pass + i += 1 + continue + + final_lines.append(rendered_line) + i += 1 + + return "\n".join(final_lines) -- 2.43.0 From 0fde5c7aa803c8b6d27762c905d1901086001a71 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 01:39:11 +0545 Subject: [PATCH 05/24] feat(djinit/templates): migrate templates to double-bracket placeholder syntax New Features - Adopt double-bracket placeholders (`[[ var ]]`) and rename templates to `.py-tpl`/.`md-tpl` extensions. - Introduce new `runtime_txt-tpl` template. Enhancements - Update `api_readme`, `models`, `apps`, `routes`, `pyproject`, `readme`, and `requirements` templates with new syntax and naming. - Align placeholder naming with a unified convention. Bug Fixes - None. --- .../components/{admin.j2 => admin.py-tpl} | 0 .../{api_readme.j2 => api_readme.md-tpl} | 6 +- djinit/templates/components/apps.j2 | 8 -- djinit/templates/components/apps.py-tpl | 6 + .../components/{models.j2 => models.py-tpl} | 2 - ...{models_readme.j2 => models_readme.md-tpl} | 2 +- djinit/templates/components/routes.j2 | 18 --- djinit/templates/components/routes.py-tpl | 8 ++ djinit/templates/components/serializers.j2 | 14 --- .../templates/components/serializers.py-tpl | 3 + .../components/{tests.j2 => tests.py-tpl} | 2 - djinit/templates/components/urls.j2 | 11 -- .../components/{views.j2 => views.py-tpl} | 2 - .../templates/config/{asgi.j2 => asgi.py-tpl} | 2 +- .../config/settings/{base.j2 => base.py-tpl} | 13 ++- .../{development.j2 => development.py-tpl} | 2 +- .../{production.j2 => production.py-tpl} | 12 +- djinit/templates/config/urls.py-tpl | 103 ++++++++++++++++++ djinit/templates/config/urls/base.j2 | 8 -- djinit/templates/config/urls/with_api.j2 | 30 ----- djinit/templates/config/urls/with_apps.j2 | 31 ------ .../templates/config/{wsgi.j2 => wsgi.py-tpl} | 2 +- .../templates/presets/predefined/api/urls.j2 | 7 -- .../presets/predefined/api/v1/urls.j2 | 7 -- .../presets/predefined/apps/generic/models.j2 | 8 -- .../predefined/apps/generic/models.py-tpl | 18 +++ .../predefined/apps/generic/serializers.j2 | 9 -- .../apps/generic/serializers.py-tpl | 11 ++ .../predefined/apps/generic/services.j2 | 10 -- .../predefined/apps/generic/services.py-tpl | 36 ++++++ .../presets/predefined/apps/generic/tests.j2 | 12 -- .../predefined/apps/generic/tests.py-tpl | 55 ++++++++++ .../presets/predefined/apps/generic/urls.j2 | 14 --- .../presets/predefined/apps/generic/views.j2 | 9 -- .../predefined/apps/generic/views.py-tpl | 62 +++++++++++ .../apps/users/{models.j2 => models.py-tpl} | 0 .../{serializers.j2 => serializers.py-tpl} | 0 .../users/{services.j2 => services.py-tpl} | 0 .../apps/users/{tests.j2 => tests.py-tpl} | 0 .../presets/predefined/apps/users/urls.j2 | 14 --- .../apps/users/{views.j2 => views.py-tpl} | 0 .../core/{exceptions.j2 => exceptions.py-tpl} | 0 ...equest_logger.j2 => request_logger.py-tpl} | 0 ...mped_model.j2 => timestamped_model.py-tpl} | 0 .../predefined/core/utils/responses.j2 | 9 -- .../core/utils/responses.py-tpl} | 8 +- .../apps/core/{apps.j2 => apps.py-tpl} | 0 .../{__init__.py.j2 => __init__.py-tpl} | 0 .../core/models/{base.py.j2 => base.py-tpl} | 0 .../models/{mixins.py.j2 => mixins.py-tpl} | 0 .../utils/{__init__.py.j2 => __init__.py-tpl} | 0 .../{github_actions.j2 => github_actions-tpl} | 4 +- .../ci/{gitlab_ci.j2 => gitlab_ci-tpl} | 14 +-- .../project/{env_sample.j2 => env_sample-tpl} | 18 +-- .../project/{gitignore.j2 => gitignore-tpl} | 0 djinit/templates/project/init.j2 | 0 .../init.py-tpl} | 0 .../project/{justfile.j2 => justfile-tpl} | 8 +- .../project/{manage_py.j2 => manage.py-tpl} | 2 +- .../project/{procfile.j2 => procfile-tpl} | 12 +- .../{pyproject_toml.j2 => pyproject.toml-tpl} | 8 +- .../project/{readme.j2 => readme.md-tpl} | 20 ++-- .../{requirements.j2 => requirements-tpl} | 8 +- djinit/templates/project/runtime_txt-tpl | 1 + djinit/templates/project/runtime_txt.j2 | 1 - 65 files changed, 378 insertions(+), 292 deletions(-) rename djinit/templates/components/{admin.j2 => admin.py-tpl} (100%) rename djinit/templates/components/{api_readme.j2 => api_readme.md-tpl} (92%) delete mode 100644 djinit/templates/components/apps.j2 create mode 100644 djinit/templates/components/apps.py-tpl rename djinit/templates/components/{models.j2 => models.py-tpl} (66%) rename djinit/templates/components/{models_readme.j2 => models_readme.md-tpl} (98%) delete mode 100644 djinit/templates/components/routes.j2 create mode 100644 djinit/templates/components/routes.py-tpl delete mode 100644 djinit/templates/components/serializers.j2 create mode 100644 djinit/templates/components/serializers.py-tpl rename djinit/templates/components/{tests.j2 => tests.py-tpl} (67%) delete mode 100644 djinit/templates/components/urls.j2 rename djinit/templates/components/{views.j2 => views.py-tpl} (68%) rename djinit/templates/config/{asgi.j2 => asgi.py-tpl} (79%) rename djinit/templates/config/settings/{base.j2 => base.py-tpl} (94%) rename djinit/templates/config/settings/{development.j2 => development.py-tpl} (97%) rename djinit/templates/config/settings/{production.j2 => production.py-tpl} (92%) create mode 100644 djinit/templates/config/urls.py-tpl delete mode 100644 djinit/templates/config/urls/base.j2 delete mode 100644 djinit/templates/config/urls/with_api.j2 delete mode 100644 djinit/templates/config/urls/with_apps.j2 rename djinit/templates/config/{wsgi.j2 => wsgi.py-tpl} (79%) delete mode 100644 djinit/templates/presets/predefined/api/urls.j2 delete mode 100644 djinit/templates/presets/predefined/api/v1/urls.j2 delete mode 100644 djinit/templates/presets/predefined/apps/generic/models.j2 create mode 100644 djinit/templates/presets/predefined/apps/generic/models.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/generic/serializers.j2 create mode 100644 djinit/templates/presets/predefined/apps/generic/serializers.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/generic/services.j2 create mode 100644 djinit/templates/presets/predefined/apps/generic/services.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/generic/tests.j2 create mode 100644 djinit/templates/presets/predefined/apps/generic/tests.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/generic/urls.j2 delete mode 100644 djinit/templates/presets/predefined/apps/generic/views.j2 create mode 100644 djinit/templates/presets/predefined/apps/generic/views.py-tpl rename djinit/templates/presets/predefined/apps/users/{models.j2 => models.py-tpl} (100%) rename djinit/templates/presets/predefined/apps/users/{serializers.j2 => serializers.py-tpl} (100%) rename djinit/templates/presets/predefined/apps/users/{services.j2 => services.py-tpl} (100%) rename djinit/templates/presets/predefined/apps/users/{tests.j2 => tests.py-tpl} (100%) delete mode 100644 djinit/templates/presets/predefined/apps/users/urls.j2 rename djinit/templates/presets/predefined/apps/users/{views.j2 => views.py-tpl} (100%) rename djinit/templates/presets/predefined/core/{exceptions.j2 => exceptions.py-tpl} (100%) rename djinit/templates/presets/predefined/core/middleware/{request_logger.j2 => request_logger.py-tpl} (100%) rename djinit/templates/presets/predefined/core/mixins/{timestamped_model.j2 => timestamped_model.py-tpl} (100%) delete mode 100644 djinit/templates/presets/predefined/core/utils/responses.j2 rename djinit/templates/presets/{unified/apps/core/utils/responses.py.j2 => predefined/core/utils/responses.py-tpl} (64%) rename djinit/templates/presets/unified/apps/core/{apps.j2 => apps.py-tpl} (100%) rename djinit/templates/presets/unified/apps/core/models/{__init__.py.j2 => __init__.py-tpl} (100%) rename djinit/templates/presets/unified/apps/core/models/{base.py.j2 => base.py-tpl} (100%) rename djinit/templates/presets/unified/apps/core/models/{mixins.py.j2 => mixins.py-tpl} (100%) rename djinit/templates/presets/unified/apps/core/utils/{__init__.py.j2 => __init__.py-tpl} (100%) rename djinit/templates/project/ci/{github_actions.j2 => github_actions-tpl} (94%) rename djinit/templates/project/ci/{gitlab_ci.j2 => gitlab_ci-tpl} (84%) rename djinit/templates/project/{env_sample.j2 => env_sample-tpl} (57%) rename djinit/templates/project/{gitignore.j2 => gitignore-tpl} (100%) delete mode 100644 djinit/templates/project/init.j2 rename djinit/templates/{components/migrations_init.j2 => project/init.py-tpl} (100%) rename djinit/templates/project/{justfile.j2 => justfile-tpl} (95%) rename djinit/templates/project/{manage_py.j2 => manage.py-tpl} (92%) rename djinit/templates/project/{procfile.j2 => procfile-tpl} (68%) rename djinit/templates/project/{pyproject_toml.j2 => pyproject.toml-tpl} (83%) rename djinit/templates/project/{readme.j2 => readme.md-tpl} (95%) rename djinit/templates/project/{requirements.j2 => requirements-tpl} (74%) create mode 100644 djinit/templates/project/runtime_txt-tpl delete mode 100644 djinit/templates/project/runtime_txt.j2 diff --git a/djinit/templates/components/admin.j2 b/djinit/templates/components/admin.py-tpl similarity index 100% rename from djinit/templates/components/admin.j2 rename to djinit/templates/components/admin.py-tpl diff --git a/djinit/templates/components/api_readme.j2 b/djinit/templates/components/api_readme.md-tpl similarity index 92% rename from djinit/templates/components/api_readme.j2 rename to djinit/templates/components/api_readme.md-tpl index 112cbe5..5d416ff 100644 --- a/djinit/templates/components/api_readme.j2 +++ b/djinit/templates/components/api_readme.md-tpl @@ -24,7 +24,7 @@ If you have a `User` model in `models/user.py`, create: ### 1. `api/user/serializers.py` ```python from rest_framework import serializers -from {{ module_name }}.models.user import User +from [[ module_name ]].models.user import User class UserSerializer(serializers.ModelSerializer): class Meta: @@ -35,7 +35,7 @@ class UserSerializer(serializers.ModelSerializer): ### 2. `api/user/views.py` ```python from rest_framework import viewsets -from {{ module_name }}.models.user import User +from [[ module_name ]].models.user import User from .serializers import UserSerializer class UserViewSet(viewsets.ModelViewSet): @@ -62,7 +62,7 @@ urlpatterns = [ from django.urls import path, include urlpatterns = [ - path('user/', include('{{ module_name }}.api.user.urls')), + path('user/', include('[[ module_name ]].api.user.urls')), # Add more API endpoints here ] ``` diff --git a/djinit/templates/components/apps.j2 b/djinit/templates/components/apps.j2 deleted file mode 100644 index bc3adcc..0000000 --- a/djinit/templates/components/apps.j2 +++ /dev/null @@ -1,8 +0,0 @@ -"""generated with djinit""" - -from django.apps import AppConfig - - -class {{ app_name|title }}Config(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = '{{ app_name }}' diff --git a/djinit/templates/components/apps.py-tpl b/djinit/templates/components/apps.py-tpl new file mode 100644 index 0000000..94f6a3d --- /dev/null +++ b/djinit/templates/components/apps.py-tpl @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class [[ app_name|title ]]Config(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = '[[ app_name ]]' diff --git a/djinit/templates/components/models.j2 b/djinit/templates/components/models.py-tpl similarity index 66% rename from djinit/templates/components/models.j2 rename to djinit/templates/components/models.py-tpl index 980c8e5..71a8362 100644 --- a/djinit/templates/components/models.j2 +++ b/djinit/templates/components/models.py-tpl @@ -1,5 +1,3 @@ -"""generated with djinit""" - from django.db import models # Create your models here. diff --git a/djinit/templates/components/models_readme.j2 b/djinit/templates/components/models_readme.md-tpl similarity index 98% rename from djinit/templates/components/models_readme.j2 rename to djinit/templates/components/models_readme.md-tpl index 867eebb..0bf247c 100644 --- a/djinit/templates/components/models_readme.j2 +++ b/djinit/templates/components/models_readme.md-tpl @@ -69,7 +69,7 @@ Create or update `admin/__init__.py`: ```python from django.contrib import admin -from {{ module_name }}.models import User +from [[ module_name ]].models import User @admin.register(User) class UserAdmin(admin.ModelAdmin): diff --git a/djinit/templates/components/routes.j2 b/djinit/templates/components/routes.j2 deleted file mode 100644 index 3831c00..0000000 --- a/djinit/templates/components/routes.j2 +++ /dev/null @@ -1,18 +0,0 @@ -""" -generated with djinit - -API routes for {{ app_name }} app. -""" - -from rest_framework.routers import DefaultRouter -from . import views - -# Create router instance -router = DefaultRouter() - -# Register viewsets with router -# Example: -# router.register(r'{{ app_name }}', views.{{ app_name|title }}ViewSet) - -# Get URL patterns -urlpatterns = router.urls diff --git a/djinit/templates/components/routes.py-tpl b/djinit/templates/components/routes.py-tpl new file mode 100644 index 0000000..37b00a9 --- /dev/null +++ b/djinit/templates/components/routes.py-tpl @@ -0,0 +1,8 @@ +from rest_framework.routers import DefaultRouter + +router = DefaultRouter() + +# Register your viewsets here +# router.register(r'endpoint', YourViewSet, basename='your-endpoint') + +urlpatterns = router.urls diff --git a/djinit/templates/components/serializers.j2 b/djinit/templates/components/serializers.j2 deleted file mode 100644 index e4b115d..0000000 --- a/djinit/templates/components/serializers.j2 +++ /dev/null @@ -1,14 +0,0 @@ -""" -generated with djinit -serializers for {{ app_name }} app. -""" - -from rest_framework import serializers - - -# Add your serializers here -# Example: -# class {{ app_name|title }}Serializer(serializers.ModelSerializer): -# class Meta: -# model = YourModel -# fields = '__all__' diff --git a/djinit/templates/components/serializers.py-tpl b/djinit/templates/components/serializers.py-tpl new file mode 100644 index 0000000..2774c36 --- /dev/null +++ b/djinit/templates/components/serializers.py-tpl @@ -0,0 +1,3 @@ +from rest_framework import serializers + +# Create your serializers here. diff --git a/djinit/templates/components/tests.j2 b/djinit/templates/components/tests.py-tpl similarity index 67% rename from djinit/templates/components/tests.j2 rename to djinit/templates/components/tests.py-tpl index 7fdd344..7ce503c 100644 --- a/djinit/templates/components/tests.j2 +++ b/djinit/templates/components/tests.py-tpl @@ -1,5 +1,3 @@ -"""generated with djinit""" - from django.test import TestCase # Create your tests here. diff --git a/djinit/templates/components/urls.j2 b/djinit/templates/components/urls.j2 deleted file mode 100644 index 501e65a..0000000 --- a/djinit/templates/components/urls.j2 +++ /dev/null @@ -1,11 +0,0 @@ -"""generated with djinit""" - -from django.urls import path -from . import views - -app_name = '{{ app_name }}' - -urlpatterns = [ - # Add your URL patterns here - # path('', views.index, name='index'), -] diff --git a/djinit/templates/components/views.j2 b/djinit/templates/components/views.py-tpl similarity index 68% rename from djinit/templates/components/views.j2 rename to djinit/templates/components/views.py-tpl index 7df6a0d..91ea44a 100644 --- a/djinit/templates/components/views.j2 +++ b/djinit/templates/components/views.py-tpl @@ -1,5 +1,3 @@ -"""generated with djinit""" - from django.shortcuts import render # Create your views here. diff --git a/djinit/templates/config/asgi.j2 b/djinit/templates/config/asgi.py-tpl similarity index 79% rename from djinit/templates/config/asgi.j2 rename to djinit/templates/config/asgi.py-tpl index 06cf14c..0c5c879 100644 --- a/djinit/templates/config/asgi.j2 +++ b/djinit/templates/config/asgi.py-tpl @@ -7,5 +7,5 @@ from django.core.asgi import get_asgi_application env = environ.Env() environ.Env.read_env() -os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="{{ project_name }}.settings.production")) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production")) application = get_asgi_application() diff --git a/djinit/templates/config/settings/base.j2 b/djinit/templates/config/settings/base.py-tpl similarity index 94% rename from djinit/templates/config/settings/base.j2 rename to djinit/templates/config/settings/base.py-tpl index 25797c9..f33b399 100644 --- a/djinit/templates/config/settings/base.j2 +++ b/djinit/templates/config/settings/base.py-tpl @@ -29,8 +29,9 @@ THIRD_PARTY_APPS = [ ] USER_DEFINED_APPS = [ -{% for app in app_names %} "{{ app }}", -{% endfor %}] +# @LOOP app in app_names "[[ app ]]", +# @ENDLOOP +] BUILT_IN_APPS = [ "django.contrib.admin", @@ -56,7 +57,7 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "{{ project_name }}.urls" +ROOT_URLCONF = "[[ project_name ]].urls" TEMPLATES = [ { @@ -73,7 +74,7 @@ TEMPLATES = [ }, ] -WSGI_APPLICATION = "{{ project_name }}.wsgi.application" +WSGI_APPLICATION = "[[ project_name ]].wsgi.application" # Password validation @@ -133,8 +134,8 @@ REST_FRAMEWORK = { # DRF Spectacular settings SPECTACULAR_SETTINGS = { - "TITLE": "{{ project_name }} API", - "DESCRIPTION": "API documentation for {{ project_name }}", + "TITLE": "[[ project_name ]] API", + "DESCRIPTION": "API documentation for [[ project_name ]]", "VERSION": "1.0.0", "SERVE_INCLUDE_SCHEMA": False, "COMPONENT_SPLIT_REQUEST": True, diff --git a/djinit/templates/config/settings/development.j2 b/djinit/templates/config/settings/development.py-tpl similarity index 97% rename from djinit/templates/config/settings/development.j2 rename to djinit/templates/config/settings/development.py-tpl index d787801..2e3c1b3 100644 --- a/djinit/templates/config/settings/development.j2 +++ b/djinit/templates/config/settings/development.py-tpl @@ -2,7 +2,7 @@ from .base import * -SECRET_KEY = "{{ secret_key }}" +SECRET_KEY = "[[ secret_key ]]" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True diff --git a/djinit/templates/config/settings/production.j2 b/djinit/templates/config/settings/production.py-tpl similarity index 92% rename from djinit/templates/config/settings/production.j2 rename to djinit/templates/config/settings/production.py-tpl index a2ab0f6..72d0c42 100644 --- a/djinit/templates/config/settings/production.j2 +++ b/djinit/templates/config/settings/production.py-tpl @@ -8,29 +8,29 @@ SECRET_KEY = env('SECRET_KEY') ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) # Database -{% if use_database_url %} +# @IF use_database_url # Using dj_database_url (recommended for production) DATABASES = { 'default': env.db('DATABASE_URL') } -{% else %} +# @ELSE # Using individual database parameters DATABASES = { 'default': { - {% if database_type == 'mysql' %} + # @IF database_type == 'mysql' 'ENGINE': 'django.db.backends.mysql', 'PORT': env('DB_PORT', default='3306'), - {% else %} + # @ELSE 'ENGINE': 'django.db.backends.postgresql', 'PORT': env('DB_PORT', default='5432'), - {% endif %} + # @ENDIF 'NAME': env('DB_NAME'), 'USER': env('DB_USER'), 'PASSWORD': env('DB_PASSWORD'), 'HOST': env('DB_HOST'), } } -{% endif %} +# @ENDIF # CORS settings for production # Filter out empty strings from FRONTEND_URL diff --git a/djinit/templates/config/urls.py-tpl b/djinit/templates/config/urls.py-tpl new file mode 100644 index 0000000..574294f --- /dev/null +++ b/djinit/templates/config/urls.py-tpl @@ -0,0 +1,103 @@ +# @IF url_type == 'project' +"""generated with djinit""" + +from django.contrib import admin +from django.urls import path +# @IF include_api or include_apps +from django.urls import include +# @ENDIF +# @IF include_api +from django.conf import settings +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularSwaggerView, +) +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenBlacklistView, +) +# @ENDIF + +urlpatterns = [ + path("admin/", admin.site.urls), +# @IF include_api + # JWT tokens + path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"), +# @ENDIF +# @IF include_api and comment_out_api_url + # path("api/", include("[[ api_module ]].urls")), +# @ENDIF +# @IF include_api and not comment_out_api_url + path("api/", include("[[ api_module ]].urls")), +# @ENDIF +# @IF include_apps + # App URLs +# @LOOP app in app_names + path("", include("[[ app ]].urls")), +# @ENDLOOP +# @ENDIF +] +# @IF include_api + +# Don't show schema in production +if settings.DEBUG: + urlpatterns += [ + path("schema/", SpectacularAPIView.as_view(), name="schema"), + path("docs/", SpectacularSwaggerView.as_view(url_name="schema")), + ] +# @ENDIF +# @ENDIF +# @IF url_type == 'api_root' +from django.urls import include, path + +app_name = "[[ app_name ]]" + +urlpatterns = [ + path("[[ version ]]/", include("[[ api_module ]].[[ version ]].urls")), +] +# @ENDIF +# @IF url_type == 'api_version' +from django.urls import include, path + +app_name = "[[ app_name ]]" + +urlpatterns = [ +# @LOOP app in app_list + path("[[ app ]]/", include("[[ app_module ]].[[ app ]].urls")), +# @ENDLOOP +] +# @ENDIF +# @IF url_type == 'drf_router' +from rest_framework.routers import DefaultRouter +# @IF has_viewset +from [[ app_module ]].views.[[ view_file ]] import [[ viewset_name ]] +# @ENDIF +# @IF has_app_name + +app_name = "[[ app_name ]]" +# @ENDIF + +router = DefaultRouter() +# @IF has_viewset +router.register(r"[[ router_prefix ]]", [[ viewset_name ]], basename="[[ basename ]]") +# @ENDIF + +urlpatterns = [ +# @IF has_include + path("", include(router.urls)), +# @ENDIF +] +# @IF not has_include +urlpatterns = router.urls +# @ENDIF +# @ENDIF +# @IF url_type == 'component' +from django.urls import path + +urlpatterns = [ + # Add your URL patterns here +] +# @ENDIF diff --git a/djinit/templates/config/urls/base.j2 b/djinit/templates/config/urls/base.j2 deleted file mode 100644 index ab41ddb..0000000 --- a/djinit/templates/config/urls/base.j2 +++ /dev/null @@ -1,8 +0,0 @@ -"""generated with djinit""" - -from django.contrib import admin -from django.urls import path - -urlpatterns = [ - path('admin/', admin.site.urls), -] diff --git a/djinit/templates/config/urls/with_api.j2 b/djinit/templates/config/urls/with_api.j2 deleted file mode 100644 index d0f0202..0000000 --- a/djinit/templates/config/urls/with_api.j2 +++ /dev/null @@ -1,30 +0,0 @@ -"""generated with djinit""" - -from django.contrib import admin -from django.urls import path, include -from django.conf import settings -from drf_spectacular.views import ( - SpectacularAPIView, - SpectacularSwaggerView, -) -from rest_framework_simplejwt.views import ( - TokenObtainPairView, - TokenRefreshView, - TokenBlacklistView, -) - -urlpatterns = [ - path("admin/", admin.site.urls), - # JWT tokens - path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), - path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), - path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"), - {% if comment_out_api_url %}# {% endif %}path("api/", include("{{ api_module }}.urls")), -] - -# Don't show schema in production -if settings.DEBUG: - urlpatterns += [ - path("schema/", SpectacularAPIView.as_view(), name="schema"), - path("docs/", SpectacularSwaggerView.as_view(url_name="schema")), - ] diff --git a/djinit/templates/config/urls/with_apps.j2 b/djinit/templates/config/urls/with_apps.j2 deleted file mode 100644 index a299544..0000000 --- a/djinit/templates/config/urls/with_apps.j2 +++ /dev/null @@ -1,31 +0,0 @@ -"""generated with djinit""" - -from django.contrib import admin -from django.urls import path, include -from django.conf import settings -from drf_spectacular.views import ( - SpectacularAPIView, - SpectacularSwaggerView, -) -from rest_framework_simplejwt.views import ( - TokenObtainPairView, - TokenRefreshView, - TokenBlacklistView, -) - -urlpatterns = [ - path("admin/", admin.site.urls), - # JWT tokens - path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), - path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), - path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"), - # App URLs -{% if comment_out_api_url %}# {% endif %} {% for app in app_names %} path("", include("{{ app }}.urls")), -{% endfor %}] - -# Don't show schema in production -if settings.DEBUG: - urlpatterns += [ - path("schema/", SpectacularAPIView.as_view(), name="schema"), - path("docs/", SpectacularSwaggerView.as_view(url_name="schema")), - ] diff --git a/djinit/templates/config/wsgi.j2 b/djinit/templates/config/wsgi.py-tpl similarity index 79% rename from djinit/templates/config/wsgi.j2 rename to djinit/templates/config/wsgi.py-tpl index de26d9b..421456d 100644 --- a/djinit/templates/config/wsgi.j2 +++ b/djinit/templates/config/wsgi.py-tpl @@ -7,6 +7,6 @@ from django.core.wsgi import get_wsgi_application env = environ.Env() environ.Env.read_env() -os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="{{ project_name }}.settings.production")) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production")) application = get_wsgi_application() diff --git a/djinit/templates/presets/predefined/api/urls.j2 b/djinit/templates/presets/predefined/api/urls.j2 deleted file mode 100644 index 51e3dfb..0000000 --- a/djinit/templates/presets/predefined/api/urls.j2 +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import include, path - -app_name = "api" - -urlpatterns = [ - path("v1/", include("api.v1.urls")), -] diff --git a/djinit/templates/presets/predefined/api/v1/urls.j2 b/djinit/templates/presets/predefined/api/v1/urls.j2 deleted file mode 100644 index 7a0d9a8..0000000 --- a/djinit/templates/presets/predefined/api/v1/urls.j2 +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import include, path - -app_name = "v1" - -urlpatterns = [ - path("users/", include("apps.users.urls")), -] diff --git a/djinit/templates/presets/predefined/apps/generic/models.j2 b/djinit/templates/presets/predefined/apps/generic/models.j2 deleted file mode 100644 index 9f99a46..0000000 --- a/djinit/templates/presets/predefined/apps/generic/models.j2 +++ /dev/null @@ -1,8 +0,0 @@ -from django.db import models - - -class {{ model_class_name }}(models.Model): - """Generated model for {{ app_name }} app.""" - - # Add fields here - pass diff --git a/djinit/templates/presets/predefined/apps/generic/models.py-tpl b/djinit/templates/presets/predefined/apps/generic/models.py-tpl new file mode 100644 index 0000000..bbae956 --- /dev/null +++ b/djinit/templates/presets/predefined/apps/generic/models.py-tpl @@ -0,0 +1,18 @@ +from django.db import models + + +class [[ model_class_name ]](models.Model): + """[[ model_class_name ]] model.""" + + # Add your fields here + name = models.CharField(max_length=255) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "[[ model_class_name ]]" + verbose_name_plural = "[[ model_class_name ]]s" + ordering = ["-created_at"] + + def __str__(self): + return self.name diff --git a/djinit/templates/presets/predefined/apps/generic/serializers.j2 b/djinit/templates/presets/predefined/apps/generic/serializers.j2 deleted file mode 100644 index 86f1b45..0000000 --- a/djinit/templates/presets/predefined/apps/generic/serializers.j2 +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework import serializers - -from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }} - - -class {{ model_class_name }}Serializer(serializers.ModelSerializer): - class Meta: - model = {{ model_class_name }} - fields = "__all__" diff --git a/djinit/templates/presets/predefined/apps/generic/serializers.py-tpl b/djinit/templates/presets/predefined/apps/generic/serializers.py-tpl new file mode 100644 index 0000000..30b7189 --- /dev/null +++ b/djinit/templates/presets/predefined/apps/generic/serializers.py-tpl @@ -0,0 +1,11 @@ +from rest_framework import serializers +from [[ app_module ]].models.[[ app_name ]] import [[ model_class_name ]] + + +class [[ model_class_name ]]Serializer(serializers.ModelSerializer): + """Serializer for [[ model_class_name ]] model.""" + + class Meta: + model = [[ model_class_name ]] + fields = "__all__" + read_only_fields = ["id", "created_at", "updated_at"] diff --git a/djinit/templates/presets/predefined/apps/generic/services.j2 b/djinit/templates/presets/predefined/apps/generic/services.j2 deleted file mode 100644 index 2203fb8..0000000 --- a/djinit/templates/presets/predefined/apps/generic/services.j2 +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Optional - -from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }} - - -def get_{{ app_name }}_by_id(object_id: int) -> Optional[{{ model_class_name }}]: - try: - return {{ model_class_name }}.objects.get(id=object_id) - except {{ model_class_name }}.DoesNotExist: - return None diff --git a/djinit/templates/presets/predefined/apps/generic/services.py-tpl b/djinit/templates/presets/predefined/apps/generic/services.py-tpl new file mode 100644 index 0000000..de45859 --- /dev/null +++ b/djinit/templates/presets/predefined/apps/generic/services.py-tpl @@ -0,0 +1,36 @@ +from [[ app_module ]].models.[[ app_name ]] import [[ model_class_name ]] + + +class [[ model_class_name ]]Service: + """Service class for [[ model_class_name ]] business logic.""" + + @staticmethod + def get_all(): + """Get all [[ model_class_name ]] instances.""" + return [[ model_class_name ]].objects.all() + + @staticmethod + def get_by_id(id): + """Get [[ model_class_name ]] by ID.""" + try: + return [[ model_class_name ]].objects.get(id=id) + except [[ model_class_name ]].DoesNotExist: + return None + + @staticmethod + def create(data): + """Create a new [[ model_class_name ]] instance.""" + return [[ model_class_name ]].objects.create(**data) + + @staticmethod + def update(instance, data): + """Update an existing [[ model_class_name ]] instance.""" + for key, value in data.items(): + setattr(instance, key, value) + instance.save() + return instance + + @staticmethod + def delete(instance): + """Delete a [[ model_class_name ]] instance.""" + instance.delete() diff --git a/djinit/templates/presets/predefined/apps/generic/tests.j2 b/djinit/templates/presets/predefined/apps/generic/tests.j2 deleted file mode 100644 index 41fc906..0000000 --- a/djinit/templates/presets/predefined/apps/generic/tests.j2 +++ /dev/null @@ -1,12 +0,0 @@ -import pytest -from django.urls import reverse -from rest_framework.test import APIClient - - -@pytest.mark.django_db -def test_{{ app_name }}_list_empty(): - client = APIClient() - url = reverse("v1:{{ app_name }}-list") - response = client.get(url) - assert response.status_code == 200 - assert response.json() == [] diff --git a/djinit/templates/presets/predefined/apps/generic/tests.py-tpl b/djinit/templates/presets/predefined/apps/generic/tests.py-tpl new file mode 100644 index 0000000..8dc7a7e --- /dev/null +++ b/djinit/templates/presets/predefined/apps/generic/tests.py-tpl @@ -0,0 +1,55 @@ +from rest_framework.test import APITestCase +from rest_framework import status +from django.contrib.auth import get_user_model + +from [[ app_module ]].models.[[ app_name ]] import [[ model_class_name ]] + +User = get_user_model() + + +class [[ model_class_name ]]APITestCase(APITestCase): + """Test cases for [[ model_class_name ]] API endpoints.""" + + def setUp(self): + """Set up test data.""" + self.user = User.objects.create_user( + username="testuser", password="testpass123" + ) + self.client.force_authenticate(user=self.user) + self.test_data = {"name": "Test [[ model_class_name ]]"} + + def test_create_[[ app_name ]](self): + """Test creating a [[ model_class_name ]].""" + response = self.client.post("/api/v1/[[ app_name ]]/", self.test_data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual([[ model_class_name ]].objects.count(), 1) + + def test_list_[[ app_name ]]s(self): + """Test listing all [[ model_class_name ]]s.""" + [[ model_class_name ]].objects.create(**self.test_data) + response = self.client.get("/api/v1/[[ app_name ]]/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_retrieve_[[ app_name ]](self): + """Test retrieving a single [[ model_class_name ]].""" + instance = [[ model_class_name ]].objects.create(**self.test_data) + response = self.client.get(f"/api/v1/[[ app_name ]]/{instance.id}/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["name"], self.test_data["name"]) + + def test_update_[[ app_name ]](self): + """Test updating a [[ model_class_name ]].""" + instance = [[ model_class_name ]].objects.create(**self.test_data) + updated_data = {"name": "Updated [[ model_class_name ]]"} + response = self.client.patch(f"/api/v1/[[ app_name ]]/{instance.id}/", updated_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + instance.refresh_from_db() + self.assertEqual(instance.name, updated_data["name"]) + + def test_delete_[[ app_name ]](self): + """Test deleting a [[ model_class_name ]].""" + instance = [[ model_class_name ]].objects.create(**self.test_data) + response = self.client.delete(f"/api/v1/[[ app_name ]]/{instance.id}/") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual([[ model_class_name ]].objects.count(), 0) diff --git a/djinit/templates/presets/predefined/apps/generic/urls.j2 b/djinit/templates/presets/predefined/apps/generic/urls.j2 deleted file mode 100644 index caec1a6..0000000 --- a/djinit/templates/presets/predefined/apps/generic/urls.j2 +++ /dev/null @@ -1,14 +0,0 @@ -from django.urls import include, path -from rest_framework.routers import DefaultRouter - -from {{ app_module }}.views.{{ app_name }}_view import {{ model_class_name }}ViewSet - - -app_name = "{{ app_name }}" - -router = DefaultRouter() -router.register(r"{{ app_name }}", {{ model_class_name }}ViewSet, basename="{{ app_name }}") - -urlpatterns = [ - path("", include(router.urls)), -] diff --git a/djinit/templates/presets/predefined/apps/generic/views.j2 b/djinit/templates/presets/predefined/apps/generic/views.j2 deleted file mode 100644 index 5976564..0000000 --- a/djinit/templates/presets/predefined/apps/generic/views.j2 +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework import viewsets - -from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }} -from {{ app_module }}.serializers.{{ app_name }}_serializer import {{ model_class_name }}Serializer - - -class {{ model_class_name }}ViewSet(viewsets.ModelViewSet): - queryset = {{ model_class_name }}.objects.all() - serializer_class = {{ model_class_name }}Serializer diff --git a/djinit/templates/presets/predefined/apps/generic/views.py-tpl b/djinit/templates/presets/predefined/apps/generic/views.py-tpl new file mode 100644 index 0000000..ef85cf5 --- /dev/null +++ b/djinit/templates/presets/predefined/apps/generic/views.py-tpl @@ -0,0 +1,62 @@ +from rest_framework import viewsets, status +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated + +from [[ app_module ]].models.[[ app_name ]] import [[ model_class_name ]] +from [[ app_module ]].serializers.[[ app_name ]]_serializer import [[ model_class_name ]]Serializer +from [[ app_module ]].services.[[ app_name ]]_service import [[ model_class_name ]]Service + + +class [[ model_class_name ]]ViewSet(viewsets.ModelViewSet): + """ViewSet for [[ model_class_name ]] CRUD operations.""" + + queryset = [[ model_class_name ]].objects.all() + serializer_class = [[ model_class_name ]]Serializer + permission_classes = [IsAuthenticated] + + def list(self, request): + """List all [[ model_class_name ]] instances.""" + instances = [[ model_class_name ]]Service.get_all() + serializer = self.get_serializer(instances, many=True) + return Response(serializer.data) + + def retrieve(self, request, pk=None): + """Retrieve a single [[ model_class_name ]] instance.""" + instance = [[ model_class_name ]]Service.get_by_id(pk) + if not instance: + return Response( + {"error": "[[ model_class_name ]] not found"}, status=status.HTTP_404_NOT_FOUND + ) + serializer = self.get_serializer(instance) + return Response(serializer.data) + + def create(self, request): + """Create a new [[ model_class_name ]] instance.""" + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = [[ model_class_name ]]Service.create(serializer.validated_data) + return Response( + self.get_serializer(instance).data, status=status.HTTP_201_CREATED + ) + + def update(self, request, pk=None): + """Update an existing [[ model_class_name ]] instance.""" + instance = [[ model_class_name ]]Service.get_by_id(pk) + if not instance: + return Response( + {"error": "[[ model_class_name ]] not found"}, status=status.HTTP_404_NOT_FOUND + ) + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + updated_instance = [[ model_class_name ]]Service.update(instance, serializer.validated_data) + return Response(self.get_serializer(updated_instance).data) + + def destroy(self, request, pk=None): + """Delete a [[ model_class_name ]] instance.""" + instance = [[ model_class_name ]]Service.get_by_id(pk) + if not instance: + return Response( + {"error": "[[ model_class_name ]] not found"}, status=status.HTTP_404_NOT_FOUND + ) + [[ model_class_name ]]Service.delete(instance) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/djinit/templates/presets/predefined/apps/users/models.j2 b/djinit/templates/presets/predefined/apps/users/models.py-tpl similarity index 100% rename from djinit/templates/presets/predefined/apps/users/models.j2 rename to djinit/templates/presets/predefined/apps/users/models.py-tpl diff --git a/djinit/templates/presets/predefined/apps/users/serializers.j2 b/djinit/templates/presets/predefined/apps/users/serializers.py-tpl similarity index 100% rename from djinit/templates/presets/predefined/apps/users/serializers.j2 rename to djinit/templates/presets/predefined/apps/users/serializers.py-tpl diff --git a/djinit/templates/presets/predefined/apps/users/services.j2 b/djinit/templates/presets/predefined/apps/users/services.py-tpl similarity index 100% rename from djinit/templates/presets/predefined/apps/users/services.j2 rename to djinit/templates/presets/predefined/apps/users/services.py-tpl diff --git a/djinit/templates/presets/predefined/apps/users/tests.j2 b/djinit/templates/presets/predefined/apps/users/tests.py-tpl similarity index 100% rename from djinit/templates/presets/predefined/apps/users/tests.j2 rename to djinit/templates/presets/predefined/apps/users/tests.py-tpl diff --git a/djinit/templates/presets/predefined/apps/users/urls.j2 b/djinit/templates/presets/predefined/apps/users/urls.j2 deleted file mode 100644 index 08a6fb4..0000000 --- a/djinit/templates/presets/predefined/apps/users/urls.j2 +++ /dev/null @@ -1,14 +0,0 @@ -from django.urls import include, path -from rest_framework.routers import DefaultRouter - -from apps.users.views.user_view import UserViewSet - - -app_name = "users" - -router = DefaultRouter() -router.register(r"users", UserViewSet, basename="users") - -urlpatterns = [ - path("", include(router.urls)), -] diff --git a/djinit/templates/presets/predefined/apps/users/views.j2 b/djinit/templates/presets/predefined/apps/users/views.py-tpl similarity index 100% rename from djinit/templates/presets/predefined/apps/users/views.j2 rename to djinit/templates/presets/predefined/apps/users/views.py-tpl diff --git a/djinit/templates/presets/predefined/core/exceptions.j2 b/djinit/templates/presets/predefined/core/exceptions.py-tpl similarity index 100% rename from djinit/templates/presets/predefined/core/exceptions.j2 rename to djinit/templates/presets/predefined/core/exceptions.py-tpl diff --git a/djinit/templates/presets/predefined/core/middleware/request_logger.j2 b/djinit/templates/presets/predefined/core/middleware/request_logger.py-tpl similarity index 100% rename from djinit/templates/presets/predefined/core/middleware/request_logger.j2 rename to djinit/templates/presets/predefined/core/middleware/request_logger.py-tpl diff --git a/djinit/templates/presets/predefined/core/mixins/timestamped_model.j2 b/djinit/templates/presets/predefined/core/mixins/timestamped_model.py-tpl similarity index 100% rename from djinit/templates/presets/predefined/core/mixins/timestamped_model.j2 rename to djinit/templates/presets/predefined/core/mixins/timestamped_model.py-tpl diff --git a/djinit/templates/presets/predefined/core/utils/responses.j2 b/djinit/templates/presets/predefined/core/utils/responses.j2 deleted file mode 100644 index bae79a1..0000000 --- a/djinit/templates/presets/predefined/core/utils/responses.j2 +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework.response import Response - - -def success(data=None, status_code=200): - return Response({"success": True, "data": data}, status=status_code) - - -def error(message, status_code=400): - return Response({"success": False, "message": message}, status=status_code) diff --git a/djinit/templates/presets/unified/apps/core/utils/responses.py.j2 b/djinit/templates/presets/predefined/core/utils/responses.py-tpl similarity index 64% rename from djinit/templates/presets/unified/apps/core/utils/responses.py.j2 rename to djinit/templates/presets/predefined/core/utils/responses.py-tpl index 929c2f6..f4b5269 100644 --- a/djinit/templates/presets/unified/apps/core/utils/responses.py.j2 +++ b/djinit/templates/presets/predefined/core/utils/responses.py-tpl @@ -1,13 +1,17 @@ +# @IF include_header """generated with djinit""" +# @ENDIF from rest_framework.response import Response def success(data=None, status_code=200): - """Return a successful response.""" +# @IF include_docstrings """Return a successful response.""" +# @ENDIF return Response({"success": True, "data": data}, status=status_code) def error(message, status_code=400): - """Return an error response.""" +# @IF include_docstrings """Return an error response.""" +# @ENDIF return Response({"success": False, "message": message}, status=status_code) diff --git a/djinit/templates/presets/unified/apps/core/apps.j2 b/djinit/templates/presets/unified/apps/core/apps.py-tpl similarity index 100% rename from djinit/templates/presets/unified/apps/core/apps.j2 rename to djinit/templates/presets/unified/apps/core/apps.py-tpl diff --git a/djinit/templates/presets/unified/apps/core/models/__init__.py.j2 b/djinit/templates/presets/unified/apps/core/models/__init__.py-tpl similarity index 100% rename from djinit/templates/presets/unified/apps/core/models/__init__.py.j2 rename to djinit/templates/presets/unified/apps/core/models/__init__.py-tpl diff --git a/djinit/templates/presets/unified/apps/core/models/base.py.j2 b/djinit/templates/presets/unified/apps/core/models/base.py-tpl similarity index 100% rename from djinit/templates/presets/unified/apps/core/models/base.py.j2 rename to djinit/templates/presets/unified/apps/core/models/base.py-tpl diff --git a/djinit/templates/presets/unified/apps/core/models/mixins.py.j2 b/djinit/templates/presets/unified/apps/core/models/mixins.py-tpl similarity index 100% rename from djinit/templates/presets/unified/apps/core/models/mixins.py.j2 rename to djinit/templates/presets/unified/apps/core/models/mixins.py-tpl diff --git a/djinit/templates/presets/unified/apps/core/utils/__init__.py.j2 b/djinit/templates/presets/unified/apps/core/utils/__init__.py-tpl similarity index 100% rename from djinit/templates/presets/unified/apps/core/utils/__init__.py.j2 rename to djinit/templates/presets/unified/apps/core/utils/__init__.py-tpl diff --git a/djinit/templates/project/ci/github_actions.j2 b/djinit/templates/project/ci/github_actions-tpl similarity index 94% rename from djinit/templates/project/ci/github_actions.j2 rename to djinit/templates/project/ci/github_actions-tpl index 7be2bd5..5217107 100644 --- a/djinit/templates/project/ci/github_actions.j2 +++ b/djinit/templates/project/ci/github_actions-tpl @@ -62,7 +62,7 @@ jobs: - name: Run Django tests env: - DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development SECRET_KEY: test-secret-key-for-ci ALLOWED_HOSTS: localhost DB_NAME: test_db @@ -76,7 +76,7 @@ jobs: - name: Check migrations env: - DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development SECRET_KEY: test-secret-key-for-ci ALLOWED_HOSTS: localhost DB_NAME: test_db diff --git a/djinit/templates/project/ci/gitlab_ci.j2 b/djinit/templates/project/ci/gitlab_ci-tpl similarity index 84% rename from djinit/templates/project/ci/gitlab_ci.j2 rename to djinit/templates/project/ci/gitlab_ci-tpl index ff6ef5b..3d3bc61 100644 --- a/djinit/templates/project/ci/gitlab_ci.j2 +++ b/djinit/templates/project/ci/gitlab_ci-tpl @@ -1,4 +1,4 @@ -# GitLab CI/CD configuration for {{ project_name }} +# GitLab CI/CD configuration for [[ project_name ]] stages: - test @@ -36,7 +36,7 @@ test: stage: test image: python:3.13 variables: - DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development SECRET_KEY: test-secret-key-for-ci ALLOWED_HOSTS: localhost script: @@ -52,7 +52,7 @@ test:python310: stage: test image: python:3.10 variables: - DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development SECRET_KEY: test-secret-key-for-ci ALLOWED_HOSTS: localhost script: @@ -69,7 +69,7 @@ test:python311: stage: test image: python:3.11 variables: - DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development SECRET_KEY: test-secret-key-for-ci ALLOWED_HOSTS: localhost script: @@ -86,7 +86,7 @@ test:python312: stage: test image: python:3.12 variables: - DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development SECRET_KEY: test-secret-key-for-ci ALLOWED_HOSTS: localhost script: @@ -103,7 +103,7 @@ test:python313: stage: test image: python:3.13 variables: - DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development SECRET_KEY: test-secret-key-for-ci ALLOWED_HOSTS: localhost script: @@ -121,7 +121,7 @@ security: stage: security image: python:3.13 variables: - DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development + DJANGO_SETTINGS_MODULE: [[ project_name ]].settings.development SECRET_KEY: test-secret-key-for-ci script: - pip install uv diff --git a/djinit/templates/project/env_sample.j2 b/djinit/templates/project/env_sample-tpl similarity index 57% rename from djinit/templates/project/env_sample.j2 rename to djinit/templates/project/env_sample-tpl index f5f2388..65d6da7 100644 --- a/djinit/templates/project/env_sample.j2 +++ b/djinit/templates/project/env_sample-tpl @@ -1,23 +1,23 @@ # Django settings -DJANGO_SETTINGS_MODULE={{ project_name }}.settings.development +DJANGO_SETTINGS_MODULE=[[ project_name ]].settings.development SECRET_KEY= your-secret-key ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com -{% if use_database_url %} +# @IF use_database_url # Database URL (recommended for production) -{% if database_type == 'mysql' %} -DATABASE_URL=mysql://user:password@localhost:3306/{{ project_name }} -{% else %} -DATABASE_URL=postgres://user:password@localhost:5432/{{ project_name }} -{% endif %} -{% else %} +# @IF database_type == 'mysql' +DATABASE_URL=mysql://user:password@localhost:3306/[[ project_name ]] +# @ELSE +DATABASE_URL=postgres://user:password@localhost:5432/[[ project_name ]] +# @ENDIF +# @ELSE # Database (individual parameters) DB_NAME= DB_USER= DB_PASSWORD= DB_HOST= DB_PORT= -{% endif %} +# @ENDIF # Frontend URL FRONTEND_URL=http://frontend.your-domain.com diff --git a/djinit/templates/project/gitignore.j2 b/djinit/templates/project/gitignore-tpl similarity index 100% rename from djinit/templates/project/gitignore.j2 rename to djinit/templates/project/gitignore-tpl diff --git a/djinit/templates/project/init.j2 b/djinit/templates/project/init.j2 deleted file mode 100644 index e69de29..0000000 diff --git a/djinit/templates/components/migrations_init.j2 b/djinit/templates/project/init.py-tpl similarity index 100% rename from djinit/templates/components/migrations_init.j2 rename to djinit/templates/project/init.py-tpl diff --git a/djinit/templates/project/justfile.j2 b/djinit/templates/project/justfile-tpl similarity index 95% rename from djinit/templates/project/justfile.j2 rename to djinit/templates/project/justfile-tpl index b5e511d..127e77d 100644 --- a/djinit/templates/project/justfile.j2 +++ b/djinit/templates/project/justfile-tpl @@ -1,4 +1,4 @@ -# Justfile for {{ project_name }} +# Justfile for [[ project_name ]] # Task runner for Django development # Install just: https://github.com/casey/just @@ -108,15 +108,15 @@ dbshell: # Start production server with gunicorn server: - uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 + uv run gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:8000 # Start production server with gunicorn workers server-prod WORKERS="4": - uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --workers {{ '{{' }}WORKERS{{ '}}' }} + uv run gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:8000 --workers {{ '{{' }}WORKERS{{ '}}' }} # Start production server with gunicorn and reload on file changes server-watch: - uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --reload + uv run gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:8000 --reload # Clean Python cache files clean: diff --git a/djinit/templates/project/manage_py.j2 b/djinit/templates/project/manage.py-tpl similarity index 92% rename from djinit/templates/project/manage_py.j2 rename to djinit/templates/project/manage.py-tpl index b6a20b7..ba6f5c7 100644 --- a/djinit/templates/project/manage_py.j2 +++ b/djinit/templates/project/manage.py-tpl @@ -11,7 +11,7 @@ import environ def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.production")) + os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "[[ project_name ]].settings.production")) try: from django.core.management import execute_from_command_line diff --git a/djinit/templates/project/procfile.j2 b/djinit/templates/project/procfile-tpl similarity index 68% rename from djinit/templates/project/procfile.j2 rename to djinit/templates/project/procfile-tpl index bb2c0db..fc01178 100644 --- a/djinit/templates/project/procfile.j2 +++ b/djinit/templates/project/procfile-tpl @@ -1,20 +1,20 @@ -# Procfile for {{ project_name }} +# Procfile for [[ project_name ]] # Used for deploying to PaaS platforms like Heroku, Railway, Render, etc. # Web server process (using Gunicorn) # Adjust workers based on your needs: (2 x CPU cores) + 1 -web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info +web: gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info # Alternative web server configurations (uncomment to use): -# web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120 -# web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120 +# web: gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120 +# web: gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120 # Release task (runs migrations before deployment) release: python manage.py migrate --noinput && python manage.py collectstatic --noinput # Uncomment below to run scheduled tasks (if using Celery) -# worker: celery -A {{ project_name }} worker --loglevel=info -# beat: celery -A {{ project_name }} beat --loglevel=info +# worker: celery -A [[ project_name ]] worker --loglevel=info +# beat: celery -A [[ project_name ]] beat --loglevel=info # Development server (for local testing with Foreman/Overmind) # web: python manage.py runserver 0.0.0.0:$PORT diff --git a/djinit/templates/project/pyproject_toml.j2 b/djinit/templates/project/pyproject.toml-tpl similarity index 83% rename from djinit/templates/project/pyproject_toml.j2 rename to djinit/templates/project/pyproject.toml-tpl index 28e3942..e9e49a3 100644 --- a/djinit/templates/project/pyproject_toml.j2 +++ b/djinit/templates/project/pyproject.toml-tpl @@ -1,5 +1,5 @@ [project] -name = "{{ package_name }}" +name = "[[ package_name ]]" version = "0.1.0" description = "" readme = "README.md" @@ -14,8 +14,12 @@ dependencies = [ "drf-spectacular", "django-cors-headers", "whitenoise", +# @IF database_type == 'mysql' "mysqlclient", +# @ENDIF +# @IF database_type == 'postgresql' "psycopg2-binary", +# @ENDIF "gunicorn", ] @@ -39,4 +43,4 @@ ignore = [ ] [tool.ruff.lint.isort] -known-first-party = ["{{ project_name }}"] +known-first-party = ["[[ project_name ]]"] diff --git a/djinit/templates/project/readme.j2 b/djinit/templates/project/readme.md-tpl similarity index 95% rename from djinit/templates/project/readme.j2 rename to djinit/templates/project/readme.md-tpl index 1aac23c..c107ade 100644 --- a/djinit/templates/project/readme.j2 +++ b/djinit/templates/project/readme.md-tpl @@ -1,4 +1,4 @@ -# {{ project_name }} +# [[ project_name ]] A Django project folder structure is generated with [djinitx/djinit/dj](https://github.com/S4NKALP/djinit). @@ -71,9 +71,9 @@ Run `just` to see all available commands, including: ## Project Structure ``` -{% if predefined_structure %} -{{ project_name }}/ -├── {{ module_name }}/ +# @IF predefined_structure +[[ project_name ]]/ +├── [[ module_name ]]/ │ ├── settings/ │ │ ├── base.py │ │ ├── development.py @@ -118,8 +118,8 @@ Run `just` to see all available commands, including: ├── Procfile ├── runtime.txt └── README.md -{% elif unified_structure %} -{{ project_name }}/ +# @ELSEIF unified_structure +[[ project_name ]]/ ├── apps/ │ ├── admin/ │ │ └── __init__.py @@ -158,9 +158,9 @@ Run `just` to see all available commands, including: ├── Procfile ├── runtime.txt └── README.md -{% else %} -{{ project_name }}/ -├── {{ module_name }}/ +# @ELSE +[[ project_name ]]/ +├── [[ module_name ]]/ │ ├── settings/ │ │ ├── base.py │ │ ├── development.py @@ -191,7 +191,7 @@ For example, if you have a `User` model in `models/user.py`, create: - `api/user/urls.py` - URL patterns for User endpoints See the README files in `api/` and `models/` directories for more details. -{% endif %} +# @ENDIF ``` ## API Documentation diff --git a/djinit/templates/project/requirements.j2 b/djinit/templates/project/requirements-tpl similarity index 74% rename from djinit/templates/project/requirements.j2 rename to djinit/templates/project/requirements-tpl index 381a0b7..43aee21 100644 --- a/djinit/templates/project/requirements.j2 +++ b/djinit/templates/project/requirements-tpl @@ -10,10 +10,10 @@ djangorestframework_simplejwt drf-spectacular django-cors-headers whitenoise -{% if database_type == 'mysql' %} +# @IF database_type == 'mysql' mysqlclient -{% else %} +# @ELSE psycopg[binary] -{% endif %} +# @ENDIF gunicorn -{% if use_database_url %}dj-database-url{% endif %} +dj-database-url # @IF use_database_url diff --git a/djinit/templates/project/runtime_txt-tpl b/djinit/templates/project/runtime_txt-tpl new file mode 100644 index 0000000..34ffefa --- /dev/null +++ b/djinit/templates/project/runtime_txt-tpl @@ -0,0 +1 @@ +python-[[ python_version ]] diff --git a/djinit/templates/project/runtime_txt.j2 b/djinit/templates/project/runtime_txt.j2 deleted file mode 100644 index 7cfeb51..0000000 --- a/djinit/templates/project/runtime_txt.j2 +++ /dev/null @@ -1 +0,0 @@ -python-{{ python_version }} -- 2.43.0 From 41782c3df01f39dd77b44bf8aeb9d702d15299ee Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 02:15:36 +0545 Subject: [PATCH 06/24] feat(djinit/utils): add replace_app_in_user_defined_apps and extend startapp with custom app_module support This change introduces a helper to replace app entries in USER_DEFINED_APPS and makes the startapp command more flexible by allowing callers to specify the app module name. New Features: - Add replace_app_in_user_defined_apps utility function in djinit/utils/common.py - Introduce app_config_name context and accept app_module argument in DjangoHelper.startapp() Enhancements: - Preserve indentation when replacing entries - Allow custom app module specification instead of defaulting to app_name Bug Fixes: (none) --- djinit/utils/common.py | 27 +++++++++++++++++++++++++++ djinit/utils/django.py | 16 ++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/djinit/utils/common.py b/djinit/utils/common.py index 4af222c..893194b 100644 --- a/djinit/utils/common.py +++ b/djinit/utils/common.py @@ -203,6 +203,33 @@ def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str: return "\n".join(new_lines) +def replace_app_in_user_defined_apps(content: str, old_app: str, new_app: str) -> str | None: + """Replace an app entry in USER_DEFINED_APPS with a new one.""" + lines = content.split("\n") + new_lines = [] + replaced = False + + for line in lines: + if not replaced and _parse_app_entry_from_line(line) == old_app: + # Try to preserve indentation + indent_idx = 0 + for char in line: + if char.isspace(): + indent_idx += 1 + else: + break + indent = line[:indent_idx] if indent_idx > 0 else " " + + new_lines.append(f'{indent}"{new_app}",') + replaced = True + else: + new_lines.append(line) + + if replaced: + return "\n".join(new_lines) + return None + + 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 diff --git a/djinit/utils/django.py b/djinit/utils/django.py index 87cb233..cecbf20 100644 --- a/djinit/utils/django.py +++ b/djinit/utils/django.py @@ -60,12 +60,24 @@ class DjangoHelper: raise DjinitError(f"Error creating Django project: {e}", details=str(e)) from e @staticmethod - def startapp(app_name: str, directory: str) -> None: + def startapp(app_name: str, directory: str, app_module: str = None) -> 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} + # Prepare context + app_config_name = app_name.title().replace('_', '') + context = { + "app_name": app_name, + "app_config_name": app_config_name, + "django_version": DjangoHelper.DJANGO_VERSION + } + + # Use provided app_module or default to app_name (path calculation handled by caller) + if not app_module: + app_module = app_name + + context["app_module"] = app_module create_init_file(app_dir, f"Created {app_name}/__init__.py") -- 2.43.0 From cab69c2232f85977cbcea6d382a57325672b4501 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 02:15:44 +0545 Subject: [PATCH 07/24] feat(djinit/templates/components): expose configurable app_config_name placeholder in AppConfig template Allows the generated AppConfig class to use a custom name derived from `app_config_name`, supporting optional `app_module` specification. New Features - Replace `app_name|title` placeholder with `app_config_name` for flexible config naming. - Add conditional block that sets `name` to `app_module` when provided, otherwise falls back to `app_name`. Enhancements - Maintain backward compatibility by defaulting to the original `app_name` behavior. - Improve template flexibility for future naming conventions. Bug Fixes - None. --- djinit/templates/components/apps.py-tpl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/djinit/templates/components/apps.py-tpl b/djinit/templates/components/apps.py-tpl index 94f6a3d..fdac009 100644 --- a/djinit/templates/components/apps.py-tpl +++ b/djinit/templates/components/apps.py-tpl @@ -1,6 +1,12 @@ from django.apps import AppConfig -class [[ app_name|title ]]Config(AppConfig): + +class [[ app_config_name ]]Config(AppConfig): default_auto_field = 'django.db.models.BigAutoField' +# @IF app_module + name = '[[ app_module ]]' +# @ENDIF +# @IF not app_module name = '[[ app_name ]]' +# @ENDIF -- 2.43.0 From f82bf1172deaecac5e961539f20b18446dbe8e95 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 02:15:50 +0545 Subject: [PATCH 08/24] feat(djinit/services): support nested apps and full AppConfig path handling - New Features - Detect and process nested project structures, generating correct module paths for AppConfig. - Automatically upgrade legacy app configurations from simple module paths to full AppConfig paths. - Enhancements - Insert full AppConfig paths into USER_DEFINED_APPS, improving template rendering for all settings files. - Provide clearer success and upgrade messages to users. - Bug Fixes - Avoid duplicate app entries and handle missing USER_DEFINED_APPS sections without crashing. - Ensure proper __init__.py creation in nested directories. --- djinit/services/app.py | 55 +++++++++++++++++++++++++++++++++----- djinit/services/files.py | 18 +++++++++++-- djinit/services/project.py | 47 +++++++++++++++++++++++++++++--- 3 files changed, 107 insertions(+), 13 deletions(-) diff --git a/djinit/services/app.py b/djinit/services/app.py index 660673c..23c18f8 100644 --- a/djinit/services/app.py +++ b/djinit/services/app.py @@ -78,8 +78,17 @@ class AppManager: 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) + is_nested, nested_dir, apps_base_dir = self._get_project_structure() + + # Calculate app_module based on project structure + if is_nested and nested_dir: + # Normalize nested_dir (replace / with . for module path) + module_base = nested_dir.replace(os.path.sep, '.') + app_module = f"{module_base}.{self.app_name}" + else: + app_module = self.app_name + + success = DjangoHelper.startapp(self.app_name, apps_base_dir, app_module) if success: UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}") else: @@ -106,23 +115,49 @@ class AppManager: is_nested, nested_dir, _ = self._get_project_structure() app_module_path = calculate_app_module_path(self.app_name, is_nested, nested_dir) + # Use full AppConfig path to properly support nested apps + # e.g. apps.users -> apps.users.apps.UsersConfig + app_config_name = self.app_name.title().replace('_', '') + full_app_config = f"{app_module_path}.apps.{app_config_name}Config" + + # We'll check if either the module path or the full config path exists + # but we'll prefer adding the full config path + existing_apps = 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") + + # Check if full config is already there + if full_app_config in existing_apps: + UIFormatter.print_success(f"App '{full_app_config}' already configured in USER_DEFINED_APPS") return True + + # Check if legacy module path is there, if so, upgrade it + if app_module_path in existing_apps: + from djinit.utils.common import replace_app_in_user_defined_apps + + UIFormatter.print_info(f"Upgrading app configuration from '{app_module_path}' to '{full_app_config}'...") + updated_content = replace_app_in_user_defined_apps(content, app_module_path, full_app_config) + + if updated_content: + with open(base_settings_path, "w") as f: + f.write(updated_content) + UIFormatter.print_success(f"Upgraded '{app_module_path}' to '{full_app_config}' in USER_DEFINED_APPS") + return True + else: + UIFormatter.print_error(f"Failed to upgrade app '{app_module_path}'") + return False if "USER_DEFINED_APPS" not in content: UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py") return False - updated_content = insert_apps_into_user_defined_apps(content, [app_module_path]) + updated_content = insert_apps_into_user_defined_apps(content, [full_app_config]) if not updated_content: return False with open(base_settings_path, "w") as f: f.write(updated_content) - UIFormatter.print_success(f"Added '{app_module_path}' to USER_DEFINED_APPS in base.py") + UIFormatter.print_success(f"Added '{full_app_config}' to USER_DEFINED_APPS in base.py") return True def _get_project_structure(self) -> tuple[bool, Optional[str], str]: @@ -192,7 +227,13 @@ class AppManager: 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} + app_config_name = self.app_name.title().replace('_', '') + base_context = { + "app_name": self.app_name, + "app_config_name": app_config_name, + "app_module": app_module, + "model_class_name": model_class_name, + } create_file_from_template( os.path.join(app_dir, "apps.py"), diff --git a/djinit/services/files.py b/djinit/services/files.py index c400af1..8dac659 100644 --- a/djinit/services/files.py +++ b/djinit/services/files.py @@ -75,10 +75,24 @@ class FileManager: secret_key = generate_secret_key() dev_context = {"secret_key": secret_key} + # Prepare context for base.py with full AppConfig paths + base_settings_context = base_context.copy() + if "app_names" in base_settings_context: + transformed_apps = [] + for app in base_settings_context["app_names"]: + # Transform to full AppConfig path: module.path -> module.path.apps.ConfigName + short_name = app.split('.')[-1] + config_name = short_name.title().replace('_', '') + 'Config' + # If it's the special "apps" container in unified structure, we might want to skip or handle differently + # But assumes all user apps follow standard djinit structure + full_path = f"{app}.apps.{config_name}" + transformed_apps.append(full_path) + base_settings_context["app_names"] = transformed_apps + for filename, context in [ - ("base.py", base_context), + ("base.py", base_settings_context), ("development.py", dev_context), - ("production.py", base_context), + ("production.py", base_settings_context), ]: filepath = os.path.join(settings_dir, filename) template_path = f"config/settings/{filename.replace('.py', '')}.py-tpl" diff --git a/djinit/services/project.py b/djinit/services/project.py index 9cc0f98..7d57071 100644 --- a/djinit/services/project.py +++ b/djinit/services/project.py @@ -46,8 +46,12 @@ class ProjectManager: 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) + # Calculate module paths for correct AppConfig generation (e.g. src.users) + app_module_paths = calculate_app_module_paths(self.app_names, self.metadata) + + for i, app_name in enumerate(self.app_names): + app_module = app_module_paths[i] + DjangoHelper.startapp(app_name, apps_base_dir, app_module) UIFormatter.print_success(f"Django app '{app_name}' created successfully!") self.add_apps_to_settings() @@ -69,10 +73,45 @@ class ProjectManager: 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] + # Transform to full AppConfig paths (e.g. apps.users.apps.UsersConfig) + full_config_paths = [] + for app in app_module_paths: + short_name = app.split('.')[-1] + config_name = short_name.title().replace('_', '') + 'Config' + full_config_paths.append(f"{app}.apps.{config_name}") + existing_apps = extract_existing_apps(content) + + # Check against both module path and full config path to avoid duplicates + apps_to_add = [] + for i, full_path in enumerate(full_config_paths): + mod_path = app_module_paths[i] + if full_path not in existing_apps and mod_path not in existing_apps: + apps_to_add.append(full_path) + elif mod_path in existing_apps: + # If legacy module path exists, we could upgrade it here, + # but ProjectManager is typically initialized on creation. + # For now, let's just not add a duplicate. + # Actually, let's upgrade it if we can + pass + + # If we found apps to add, insert them if not apps_to_add: + # Check if we need to upgrade any legacy paths + upgraded = False + for i, full_path in enumerate(full_config_paths): + mod_path = app_module_paths[i] + if mod_path in existing_apps and full_path not in existing_apps: + from djinit.utils.common import replace_app_in_user_defined_apps + content = replace_app_in_user_defined_apps(content, mod_path, full_path) + upgraded = True + + if upgraded: + with open(base_settings_path, "w") as f: + f.write(content) + UIFormatter.print_success("Upgraded existing apps to full AppConfig paths") + return + UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS") return -- 2.43.0 From 64edec1cc21e11f9a56932d2311de3be93dd0f31 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 02:17:46 +0545 Subject: [PATCH 09/24] feat(djinit/core): bump Django version to 6.0.1 This update aligns generated projects with Django 6.0.1, providing access to the latest framework improvements and security patches. --- djinit/core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djinit/core/config.py b/djinit/core/config.py index 24e0e86..61b610e 100644 --- a/djinit/core/config.py +++ b/djinit/core/config.py @@ -5,7 +5,7 @@ Core configuration and constants for djinit. import sys # Django version used for project generation -DJANGO_VERSION = "5.2" +DJANGO_VERSION = "6.0.1" # Reserved names that cannot be used for projects or apps DJANGO_RESERVED = {"django", "test", "site-packages", "admin"} -- 2.43.0 From e5f05681d198a61f0180c917a12125c65c5b3162 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 02:48:13 +0545 Subject: [PATCH 10/24] feat(djinit): introduce base service abstractions and refactor template engine Add foundational class hierarchy and refactor the template engine to use inheritance and common utilities, enhancing extensibility and code organization. - New Feature: Added BaseObject, BaseService, BaseCommand, and BaseUtils classes in djinit/core/base.py providing common interfaces. - Enhancement: Refactored TemplateEngine to inherit BaseService and utilize BaseUtils for logic parsing. - Enhancement: Updated InFileLogicParser to enforce proper stack handling and simplified conditional block processing. - Enhancement: Consolidated utility functions via CommonUtils in generator and ProjectManager, streamlining app configuration upgrades. --- djinit/core/base.py | 52 +++++++++++++++++ djinit/core/parser.py | 18 +++--- djinit/generator.py | 8 ++- djinit/services/app.py | 79 ++++++++++++------------- djinit/services/files.py | 109 ++++++++++++++++++++--------------- djinit/services/project.py | 58 +++++++++---------- djinit/services/templates.py | 7 ++- 7 files changed, 201 insertions(+), 130 deletions(-) create mode 100644 djinit/core/base.py diff --git a/djinit/core/base.py b/djinit/core/base.py new file mode 100644 index 0000000..452528a --- /dev/null +++ b/djinit/core/base.py @@ -0,0 +1,52 @@ +""" +Base classes for the djinit architecture. +Defines the common interface for services, commands, and utilities. +""" + +from abc import abstractmethod +from typing import Any + + +class BaseObject: + """ + Base object for all djinit classes. + """ + + def __init__(self, *args, **kwargs): + pass + + +class BaseService(BaseObject): + """ + Base class for all services. + Services handle business logic and orchestration. + """ + + def __init__(self, project_root: str = None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.project_root = project_root + + +class BaseCommand(BaseObject): + """ + Base class for all CLI commands. + Commands handle user input and invoke services. + """ + + def __init__(self, args: Any, *args_list, **kwargs): + super().__init__(*args_list, **kwargs) + self.args = args + + @abstractmethod + def execute(self) -> None: + """Execute the command.""" + pass + + +class BaseUtils(BaseObject): + """ + Base class for utility collections. + Utilities provide helper functions. + """ + + pass diff --git a/djinit/core/parser.py b/djinit/core/parser.py index 542ad44..5be47bf 100644 --- a/djinit/core/parser.py +++ b/djinit/core/parser.py @@ -33,7 +33,7 @@ class InFileLogicParser: # Stack stores boolean results of nested IF blocks # Each element is (current_block_result, has_any_true_branch_executed) stack: List[List[bool]] = [] - + lines = template_text.splitlines() i = 0 while i < len(lines): @@ -57,10 +57,10 @@ class InFileLogicParser: final_lines.append(line) i += 1 continue - + expr = stripped[9:].strip() current_stack = stack[-1] - + if current_stack[1]: current_stack[0] = False else: @@ -80,7 +80,7 @@ class InFileLogicParser: final_lines.append(line) i += 1 continue - + current_stack = stack[-1] if current_stack[1]: current_stack[0] = False @@ -121,7 +121,7 @@ class InFileLogicParser: iterable = eval(iterable_name, {"__builtins__": {}}, self.context) except Exception: iterable = [] - + # Capture loop body loop_body = [] i += 1 @@ -131,12 +131,12 @@ class InFileLogicParser: loop_depth += 1 elif lines[i].strip().startswith("# @ENDLOOP"): loop_depth -= 1 - + if loop_depth > 0: loop_body.append(lines[i]) i += 1 - - if i < len(lines): # Skip the @ENDLOOP line itself + + if i < len(lines): # Skip the @ENDLOOP line itself i += 1 # Execute loop @@ -181,7 +181,7 @@ class InFileLogicParser: parts = rendered_line.split("# @IF ", 1) pre_content = parts[0].rstrip() rest = parts[1].strip() - + # Split rest into expression and post-content (if any) # This is tricky because the expression might have spaces. # If there's an # @ENDIF on the same line, use it. diff --git a/djinit/generator.py b/djinit/generator.py index 16779ac..660746f 100644 --- a/djinit/generator.py +++ b/djinit/generator.py @@ -8,7 +8,9 @@ import subprocess import sys from djinit.cli import Cli -from djinit.commands.handler import handle_app_command, handle_secret_command, parse_arguments +from djinit.commands.app import AppCommand +from djinit.commands.handler import parse_arguments +from djinit.commands.secret import SecretCommand from djinit.ui.console import UIFormatter, console from djinit.ui.input import confirm_setup, get_user_input @@ -27,11 +29,11 @@ def main() -> None: args = parse_arguments() if args.command == "secret": - handle_secret_command(args) + SecretCommand(args).execute() return if args.command == "app": - handle_app_command(args) + AppCommand(args).execute() return clear_screen() diff --git a/djinit/services/app.py b/djinit/services/app.py index 23c18f8..66a69ab 100644 --- a/djinit/services/app.py +++ b/djinit/services/app.py @@ -6,29 +6,19 @@ Handles creation of Django apps and updating settings. import os from typing import Optional +from djinit.core.base import BaseService from djinit.ui.console import UIFormatter -from djinit.utils.common import ( - 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.common import CommonUtils from djinit.utils.django import DjangoHelper -class AppManager: +class AppManager(BaseService): def __init__(self, app_name: str): + super().__init__() self.app_name = app_name self.current_dir = os.getcwd() self.manage_py_path = os.path.join(self.current_dir, "manage.py") - self.config = get_djinit_config(self.current_dir) + self.config = CommonUtils.get_djinit_config(self.current_dir) self._project_structure_cache = None def create_app(self) -> bool: @@ -59,7 +49,7 @@ class AppManager: return True def _is_django_project(self) -> bool: - return is_django_project(self.current_dir) + return CommonUtils.is_django_project(self.current_dir) def _app_exists(self) -> bool: # For predefined structure, check in apps/ directory @@ -79,15 +69,15 @@ class AppManager: return self._create_predefined_app(os.path.join(self.current_dir, "apps")) is_nested, nested_dir, apps_base_dir = self._get_project_structure() - + # Calculate app_module based on project structure if is_nested and nested_dir: # Normalize nested_dir (replace / with . for module path) - module_base = nested_dir.replace(os.path.sep, '.') + module_base = nested_dir.replace(os.path.sep, ".") app_module = f"{module_base}.{self.app_name}" else: app_module = self.app_name - + success = DjangoHelper.startapp(self.app_name, apps_base_dir, app_module) if success: UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}") @@ -96,7 +86,7 @@ class AppManager: return success def _add_to_installed_apps(self) -> bool: - settings_path = find_settings_path(self.current_dir) + settings_path = CommonUtils.find_settings_path(self.current_dir) if not settings_path: UIFormatter.print_error("Could not find Django settings directory") return False @@ -113,30 +103,28 @@ class AppManager: 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) + app_module_path = CommonUtils.calculate_app_module_path(self.app_name, is_nested, nested_dir) # Use full AppConfig path to properly support nested apps # e.g. apps.users -> apps.users.apps.UsersConfig - app_config_name = self.app_name.title().replace('_', '') + app_config_name = self.app_name.title().replace("_", "") full_app_config = f"{app_module_path}.apps.{app_config_name}Config" - + # We'll check if either the module path or the full config path exists # but we'll prefer adding the full config path - existing_apps = extract_existing_apps(content) - + existing_apps = CommonUtils.extract_existing_apps(content) + # Check if full config is already there if full_app_config in existing_apps: UIFormatter.print_success(f"App '{full_app_config}' already configured in USER_DEFINED_APPS") return True - + # Check if legacy module path is there, if so, upgrade it if app_module_path in existing_apps: - from djinit.utils.common import replace_app_in_user_defined_apps - UIFormatter.print_info(f"Upgrading app configuration from '{app_module_path}' to '{full_app_config}'...") - updated_content = replace_app_in_user_defined_apps(content, app_module_path, full_app_config) - + updated_content = CommonUtils.replace_app_in_user_defined_apps(content, app_module_path, full_app_config) + if updated_content: with open(base_settings_path, "w") as f: f.write(updated_content) @@ -150,7 +138,7 @@ class AppManager: UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py") return False - updated_content = insert_apps_into_user_defined_apps(content, [full_app_config]) + updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, [full_app_config]) if not updated_content: return False @@ -177,12 +165,12 @@ class AppManager: 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) + project_dir, settings_base_path = CommonUtils.find_project_dir(self.current_dir) if project_dir is None: self._project_structure_cache = (False, None, self.current_dir) else: - self._project_structure_cache = detect_nested_structure_from_settings( + self._project_structure_cache = CommonUtils.detect_nested_structure_from_settings( settings_base_path, self.current_dir ) return self._project_structure_cache @@ -210,7 +198,7 @@ class AppManager: return False # Check for Single Folder structure (models/ and api/ exist in project dir) - project_dir, _ = find_project_dir(self.current_dir) + project_dir, _ = CommonUtils.find_project_dir(self.current_dir) if project_dir: has_models = os.path.isdir(os.path.join(project_dir, "models")) has_api = os.path.isdir(os.path.join(project_dir, "api")) @@ -223,11 +211,11 @@ class AppManager: """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") + CommonUtils.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}" - app_config_name = self.app_name.title().replace('_', '') + app_config_name = self.app_name.title().replace("_", "") base_context = { "app_name": self.app_name, "app_config_name": app_config_name, @@ -235,7 +223,7 @@ class AppManager: "model_class_name": model_class_name, } - create_file_from_template( + CommonUtils.create_file_from_template( os.path.join(app_dir, "apps.py"), "components/apps.py-tpl", {"app_name": self.app_name}, @@ -251,17 +239,26 @@ class AppManager: } 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") + CommonUtils.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( + CommonUtils.create_file_from_template( file_path, template, base_context, f"Created apps/{self.app_name}/{folder}/{filename}" ) - create_file_from_template( + CommonUtils.create_file_from_template( os.path.join(app_dir, "urls.py"), "config/urls.py-tpl", - {"url_type": "drf_router", "app_module": app_module, "view_file": f"{self.app_name}_view", "viewset_name": f"{model_class_name}ViewSet", "router_prefix": "", "basename": self.app_name, "has_viewset": True, "has_include": False}, + { + "url_type": "drf_router", + "app_module": app_module, + "view_file": f"{self.app_name}_view", + "viewset_name": f"{model_class_name}ViewSet", + "router_prefix": "", + "basename": self.app_name, + "has_viewset": True, + "has_include": False, + }, f"Created apps/{self.app_name}/urls.py", ) diff --git a/djinit/services/files.py b/djinit/services/files.py index 8dac659..dc32929 100644 --- a/djinit/services/files.py +++ b/djinit/services/files.py @@ -5,20 +5,14 @@ Handles creation and modification of project files. import os +from djinit.core.base import BaseService from djinit.services.templates import template_engine -from djinit.utils.common import ( - calculate_app_module_paths, - change_cwd, - create_directory_with_init, - create_file_from_template, - create_file_with_content, - get_package_name, -) +from djinit.utils.common import CommonUtils -class FileManager: +class FileManager(BaseService): def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict): - self.project_root = project_root + super().__init__(project_root=project_root) self.project_name = project_name self.app_names = app_names self.metadata = metadata @@ -38,9 +32,9 @@ class FileManager: ) -> None: """Helper to render template and create file.""" target_dir = base_dir or self.project_root - with change_cwd(target_dir): + with CommonUtils.change_cwd(target_dir): content = template_engine.render_template(template_path, context) - create_file_with_content(filepath, content, message, should_format=should_format) + CommonUtils.create_file_with_content(filepath, content, message, should_format=should_format) def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None: """Helper to create multiple folders and files from a specification dict.""" @@ -48,25 +42,27 @@ class FileManager: for folder_name, file_templates in folder_specs.items(): folder_path = os.path.join(base_dir, folder_name) - create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py") + CommonUtils.create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py") for filename, template_path in file_templates: file_path = os.path.join(folder_path, filename) - create_file_from_template(file_path, template_path, base_context, f"Created {file_path}") + CommonUtils.create_file_from_template(file_path, template_path, base_context, f"Created {file_path}") def _create_apps_py(self, app_dir: str, app_name: str, message: str = None, should_format: bool = False) -> None: """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.py-tpl", context, msg, should_format=should_format) + CommonUtils.create_file_from_template( + apps_py_path, "components/apps.py-tpl", context, msg, should_format=should_format + ) def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None: """Helper to create multiple subdirectories with __init__.py files.""" for subdir in subdirs: subdir_path = os.path.join(base_dir, subdir) message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py" - create_directory_with_init(subdir_path, message) + CommonUtils.create_directory_with_init(subdir_path, message) def _create_settings_package(self, settings_dir: str, base_context: dict, prefix: str) -> None: """Helper to create settings package files (base, development, production).""" @@ -81,8 +77,8 @@ class FileManager: transformed_apps = [] for app in base_settings_context["app_names"]: # Transform to full AppConfig path: module.path -> module.path.apps.ConfigName - short_name = app.split('.')[-1] - config_name = short_name.title().replace('_', '') + 'Config' + short_name = app.split(".")[-1] + config_name = short_name.title().replace("_", "") + "Config" # If it's the special "apps" container in unified structure, we might want to skip or handle differently # But assumes all user apps follow standard djinit structure full_path = f"{app}.apps.{config_name}" @@ -169,7 +165,7 @@ class FileManager: def create_pyproject(self, metadata: dict) -> None: package_name = metadata.get("package_name", "backend") - package_name = get_package_name(package_name) + package_name = CommonUtils.get_package_name(package_name) context = { "package_name": package_name, "project_name": self.project_name, @@ -184,7 +180,7 @@ class FileManager: def create_project_urls(self) -> None: """Create project urls.py using project_config/urls.py-tpl template (replaces update_project_urls).""" - effective_app_modules = calculate_app_module_paths(self.app_names, self.metadata) + effective_app_modules = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata) context = { "url_type": "project", "project_name": self.project_name, @@ -212,10 +208,10 @@ class FileManager: 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") + CommonUtils.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") + CommonUtils.create_directory_with_init(users_dir, "Created apps/users/__init__.py") self._create_apps_py(users_dir, "users") users_subfolders = { @@ -228,15 +224,26 @@ class FileManager: 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( + CommonUtils.create_file_from_template( users_urls_path, "config/urls.py-tpl", - {"url_type": "drf_router", "app_module": "apps.users", "view_file": "user_view", "viewset_name": "UserViewSet", "router_prefix": "users", "basename": "users", "has_viewset": True, "has_app_name": True, "app_name": "users", "has_include": True}, + { + "url_type": "drf_router", + "app_module": "apps.users", + "view_file": "user_view", + "viewset_name": "UserViewSet", + "router_prefix": "users", + "basename": "users", + "has_viewset": True, + "has_app_name": True, + "app_name": "users", + "has_include": True, + }, "Created apps/users/urls.py", ) core_dir = os.path.join(apps_dir, "core") - create_directory_with_init(core_dir, "Created apps/core/__init__.py") + CommonUtils.create_directory_with_init(core_dir, "Created apps/core/__init__.py") core_subfolders = { "utils": [("responses.py", "presets/predefined/core/utils/responses.py-tpl")], @@ -246,22 +253,32 @@ class FileManager: self._create_files_from_specs(core_dir, core_subfolders, {}) exceptions_path = os.path.join(core_dir, "exceptions.py") - create_file_from_template( + CommonUtils.create_file_from_template( exceptions_path, "presets/predefined/core/exceptions.py-tpl", {}, "Created apps/core/exceptions.py" ) api_dir = os.path.join(self.project_root, "api") - create_directory_with_init(api_dir, "Created api/__init__.py") + CommonUtils.create_directory_with_init(api_dir, "Created api/__init__.py") api_urls_path = os.path.join(api_dir, "urls.py") - create_file_from_template(api_urls_path, "config/urls.py-tpl", {"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "api"}, "Created api/urls.py") + CommonUtils.create_file_from_template( + api_urls_path, + "config/urls.py-tpl", + {"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "api"}, + "Created api/urls.py", + ) api_v1_dir = os.path.join(api_dir, "v1") - create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py") + CommonUtils.create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py") api_v1_urls_path = os.path.join(api_v1_dir, "urls.py") - create_file_from_template(api_v1_urls_path, "config/urls.py-tpl", {"url_type": "api_version", "app_name": "v1", "app_list": ["users"], "app_module": "apps"}, "Created api/v1/urls.py") + CommonUtils.create_file_from_template( + api_v1_urls_path, + "config/urls.py-tpl", + {"url_type": "api_version", "app_name": "v1", "app_list": ["users"], "app_module": "apps"}, + "Created api/v1/urls.py", + ) project_urls_path = os.path.join(self.project_configs, "urls.py") - create_file_from_template( + CommonUtils.create_file_from_template( project_urls_path, "config/urls.py-tpl", {"url_type": "project", "api_module": "api", "include_api": True}, @@ -272,10 +289,10 @@ class FileManager: def create_unified_structure(self) -> None: # 1. Create 'core' directory (Project Config) core_dir = os.path.join(self.project_root, "core") - create_directory_with_init(core_dir, "Created core/__init__.py") + CommonUtils.create_directory_with_init(core_dir, "Created core/__init__.py") settings_dir = os.path.join(core_dir, "settings") - create_directory_with_init(settings_dir, "Created core/settings/__init__.py") + CommonUtils.create_directory_with_init(settings_dir, "Created core/settings/__init__.py") # Create settings files base_context = { @@ -291,7 +308,7 @@ class FileManager: # 2. Create 'apps' directory (The Main App) apps_dir = os.path.join(self.project_root, "apps") - create_directory_with_init(apps_dir, "Created apps/__init__.py") + CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py") # Create apps.py for the 'apps' app self._create_apps_py(apps_dir, "apps", "Created apps/apps.py", should_format=True) @@ -302,9 +319,9 @@ class FileManager: # Create 'api' directory with v1 api_dir = os.path.join(apps_dir, "api") - create_directory_with_init(api_dir, "Created apps/api/__init__.py") + CommonUtils.create_directory_with_init(api_dir, "Created apps/api/__init__.py") - create_file_from_template( + CommonUtils.create_file_from_template( os.path.join(api_dir, "urls.py"), "config/urls.py-tpl", {"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "apps.api"}, @@ -313,9 +330,9 @@ class FileManager: ) api_v1_dir = os.path.join(api_dir, "v1") - create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py") + CommonUtils.create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py") - create_file_from_template( + CommonUtils.create_file_from_template( os.path.join(api_v1_dir, "urls.py"), "config/urls.py-tpl", {"url_type": "api_version", "app_name": "v1", "app_list": [], "app_module": "apps"}, @@ -325,10 +342,10 @@ class FileManager: 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") + CommonUtils.create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py") settings_dir = os.path.join(project_dir, "settings") - create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py") + CommonUtils.create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py") # Create settings files base_context = { @@ -347,28 +364,28 @@ class FileManager: components = ["admin", "api", "models", "tests"] self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/") - create_file_from_template( + CommonUtils.create_file_from_template( os.path.join(project_dir, "admin", "__init__.py"), "components/admin.py-tpl", {}, f"Created {self.module_name}/admin/__init__.py", ) - create_file_from_template( + CommonUtils.create_file_from_template( os.path.join(project_dir, "models", "__init__.py"), "components/models.py-tpl", {}, f"Created {self.module_name}/models/__init__.py", ) - create_file_from_template( + CommonUtils.create_file_from_template( os.path.join(project_dir, "api", "README.md"), "components/api_readme.md-tpl", {"module_name": self.module_name}, f"Created {self.module_name}/api/README.md", ) - create_file_from_template( + CommonUtils.create_file_from_template( os.path.join(project_dir, "models", "README.md"), "components/models_readme.md-tpl", {"module_name": self.module_name}, @@ -447,4 +464,6 @@ class FileManager: } filepath = os.path.join(self.project_root, ".djinit") - create_file_with_content(filepath, json.dumps(config, indent=4), "Created .djinit configuration file") + CommonUtils.create_file_with_content( + filepath, json.dumps(config, indent=4), "Created .djinit configuration file" + ) diff --git a/djinit/services/project.py b/djinit/services/project.py index 7d57071..af17eb3 100644 --- a/djinit/services/project.py +++ b/djinit/services/project.py @@ -5,25 +5,22 @@ Handles creation of Django projects and apps. import os +from djinit.core.base import BaseService from djinit.ui.console import UIFormatter -from djinit.utils.common import ( - calculate_app_module_paths, - create_directory_with_init, - extract_existing_apps, - get_base_settings_path, - insert_apps_into_user_defined_apps, - read_base_settings, -) +from djinit.utils.common import CommonUtils from djinit.utils.django import DjangoHelper -class ProjectManager: +class ProjectManager(BaseService): def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict): + super().__init__(project_root=os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)) self.project_dir = project_dir self.project_name = project_name self.app_names = app_names self.metadata = metadata - self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir) + # self.project_root is set by BaseService via super().__init__ but we need to ensure it matches logic + # logic was: self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir) + # which I passed to super. So I can remove the line setting self.project_root here. self.module_name = metadata.get("project_module_name") or self.project_name def create_project(self) -> None: @@ -44,10 +41,12 @@ class ProjectManager: apps_base_dir = self._get_apps_base_dir() if apps_base_dir != self.project_root: - create_directory_with_init(apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py") + CommonUtils.create_directory_with_init( + apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py" + ) # Calculate module paths for correct AppConfig generation (e.g. src.users) - app_module_paths = calculate_app_module_paths(self.app_names, self.metadata) + app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata) for i, app_name in enumerate(self.app_names): app_module = app_module_paths[i] @@ -58,30 +57,30 @@ class ProjectManager: def add_apps_to_settings(self) -> None: """Add all apps to USER_DEFINED_APPS in base.py settings file.""" - base_settings_path = get_base_settings_path(self.project_root, self.module_name) + base_settings_path = CommonUtils.get_base_settings_path(self.project_root, self.module_name) if not os.path.exists(base_settings_path): from djinit.utils.exceptions import ConfigError raise ConfigError("Could not find base.py settings file") - content = read_base_settings(self.project_root, self.module_name) + content = CommonUtils.read_base_settings(self.project_root, self.module_name) if content is None: from djinit.utils.exceptions import ConfigError raise ConfigError("Could not read base.py settings file") - app_module_paths = calculate_app_module_paths(self.app_names, self.metadata) + app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata) # Transform to full AppConfig paths (e.g. apps.users.apps.UsersConfig) full_config_paths = [] for app in app_module_paths: - short_name = app.split('.')[-1] - config_name = short_name.title().replace('_', '') + 'Config' + short_name = app.split(".")[-1] + config_name = short_name.title().replace("_", "") + "Config" full_config_paths.append(f"{app}.apps.{config_name}") - existing_apps = extract_existing_apps(content) - + existing_apps = CommonUtils.extract_existing_apps(content) + # Check against both module path and full config path to avoid duplicates apps_to_add = [] for i, full_path in enumerate(full_config_paths): @@ -89,11 +88,11 @@ class ProjectManager: if full_path not in existing_apps and mod_path not in existing_apps: apps_to_add.append(full_path) elif mod_path in existing_apps: - # If legacy module path exists, we could upgrade it here, + # If legacy module path exists, we could upgrade it here, # but ProjectManager is typically initialized on creation. # For now, let's just not add a duplicate. - # Actually, let's upgrade it if we can - pass + # Actually, let's upgrade it if we can + pass # If we found apps to add, insert them if not apps_to_add: @@ -102,20 +101,19 @@ class ProjectManager: for i, full_path in enumerate(full_config_paths): mod_path = app_module_paths[i] if mod_path in existing_apps and full_path not in existing_apps: - from djinit.utils.common import replace_app_in_user_defined_apps - content = replace_app_in_user_defined_apps(content, mod_path, full_path) - upgraded = True + content = CommonUtils.replace_app_in_user_defined_apps(content, mod_path, full_path) + upgraded = True if upgraded: - with open(base_settings_path, "w") as f: + with open(base_settings_path, "w") as f: f.write(content) - UIFormatter.print_success("Upgraded existing apps to full AppConfig paths") - return - + UIFormatter.print_success("Upgraded existing apps to full AppConfig paths") + return + UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS") return - updated_content = insert_apps_into_user_defined_apps(content, apps_to_add) + updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, apps_to_add) if not updated_content: from djinit.utils.exceptions import ConfigError diff --git a/djinit/services/templates.py b/djinit/services/templates.py index e7a7f0b..69a3e9f 100644 --- a/djinit/services/templates.py +++ b/djinit/services/templates.py @@ -7,10 +7,11 @@ import os from pathlib import Path from typing import Any, Dict +from djinit.core.base import BaseService from djinit.core.parser import InFileLogicParser -class TemplateEngine: +class TemplateEngine(BaseService): """Template engine for rendering file templates.""" def __init__(self, template_dir: str = None): @@ -23,6 +24,8 @@ class TemplateEngine: # djinit/services/templates.py -> djinit/services -> djinit -> templates template_dir = Path(__file__).resolve().parent.parent / "templates" + super().__init__() + if not template_dir.exists(): raise FileNotFoundError(f"Template directory not found: {template_dir}") @@ -43,7 +46,7 @@ class TemplateEngine: if not os.path.exists(template_path): raise FileNotFoundError(f"Template not found: {template_path}") - with open(template_path, "r") as f: + with open(template_path) as f: template_text = f.read() return self.parser.render(template_text, context) -- 2.43.0 From 995d0624e4f4fca60f63d2cf7e24bd366229b103 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 02:48:18 +0545 Subject: [PATCH 11/24] feat(djinit/commands): add dedicated commands for app creation and secret key generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Features - Added `app.py` command to scaffold Django apps with name validation and post‑creation instructions. - Added `secret.py` command to generate configurable secret keys and display usage guidance. Enhancements - Refactored command dispatch in `handler.py` to delegate to specialized command classes, removing duplicated logic. - Improved modularity and maintainability of command implementation. --- djinit/commands/app.py | 64 +++++++++++++++++++++++++++++++++ djinit/commands/handler.py | 73 ++++---------------------------------- djinit/commands/secret.py | 34 ++++++++++++++++++ 3 files changed, 104 insertions(+), 67 deletions(-) create mode 100644 djinit/commands/app.py create mode 100644 djinit/commands/secret.py diff --git a/djinit/commands/app.py b/djinit/commands/app.py new file mode 100644 index 0000000..875d18e --- /dev/null +++ b/djinit/commands/app.py @@ -0,0 +1,64 @@ +""" +App creation command. +""" + +import sys + +from rich.panel import Panel +from rich.text import Text + +from djinit.core.base import BaseCommand +from djinit.services.app import AppManager +from djinit.ui.console import UIColors, UIFormatter, console +from djinit.utils.validators import validate_app_name + + +class AppCommand(BaseCommand): + """Command to create Django apps.""" + + def execute(self) -> None: + UIFormatter.print_info("") + UIFormatter.print_header("Django App Creation") + UIFormatter.print_info("") + + raw_tokens = self.args.app_name if isinstance(self.args.app_name, list) else [self.args.app_name] + + # Flatten and clean tokens + app_names = [part.strip() for token in raw_tokens if token for part in str(token).split(",") if part.strip()] + + if not app_names: + UIFormatter.print_error("App name cannot be empty") + sys.exit(1) + + # Deduplicate while preserving order + deduped_names = list(dict.fromkeys(app_names)) + + for name in deduped_names: + is_valid, error_msg = validate_app_name(name) + if not is_valid: + UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}") + sys.exit(1) + + any_failure = False + for name in deduped_names: + app_manager = AppManager(name) + success = app_manager.create_app() + if not success: + UIFormatter.print_error(f"Failed to create Django app '{name}'") + any_failure = True + break + UIFormatter.print_success(f"Django app '{name}' created successfully!") + + if any_failure: + sys.exit(1) + + instructions = Text() + instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT) + instructions.append("1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n") + instructions.append("2. Create your models in each app's models.py\n") + instructions.append("3. Run migrations: just makemigrations\n") + instructions.append("4. Apply migrations: just migrate\n") + instructions.append("5. Create views, serializers and routes(URLs) for your app(s)\n") + + console.print(Panel(instructions, title="💡 What's Next", border_style="green")) + UIFormatter.print_info("") diff --git a/djinit/commands/handler.py b/djinit/commands/handler.py index 5e84f34..c30a767 100644 --- a/djinit/commands/handler.py +++ b/djinit/commands/handler.py @@ -4,15 +4,6 @@ Handles different commands like secret key generation and app creation. """ import argparse -import sys - -from rich.panel import Panel -from rich.text import Text - -from djinit.services.app import AppManager -from djinit.ui.console import UIColors, UIFormatter, console -from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys -from djinit.utils.validators import validate_app_name def parse_arguments() -> argparse.Namespace: @@ -45,66 +36,14 @@ def parse_arguments() -> argparse.Namespace: def handle_secret_command(args: argparse.Namespace) -> None: - keys = generate_multiple_keys(args.count, args.length) - UIFormatter.print_info("") - display_secret_keys(keys) + from djinit.commands.secret import SecretCommand - 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() + command = SecretCommand(args) + command.execute() def handle_app_command(args: argparse.Namespace) -> None: - UIFormatter.print_info("") - UIFormatter.print_header("Django App Creation") - UIFormatter.print_info("") + from djinit.commands.app import AppCommand - raw_tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name] - - # Flatten and clean tokens - app_names = [part.strip() for token in raw_tokens if token for part in str(token).split(",") if part.strip()] - - if not app_names: - UIFormatter.print_error("App name cannot be empty") - sys.exit(1) - - # Deduplicate while preserving order - deduped_names = list(dict.fromkeys(app_names)) - - for name in deduped_names: - is_valid, error_msg = validate_app_name(name) - if not is_valid: - UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}") - sys.exit(1) - - any_failure = False - for name in deduped_names: - app_manager = AppManager(name) - success = app_manager.create_app() - if not success: - UIFormatter.print_error(f"Failed to create Django app '{name}'") - any_failure = True - break - UIFormatter.print_success(f"Django app '{name}' created successfully!") - - if any_failure: - sys.exit(1) - - instructions = Text() - instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT) - instructions.append("1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n") - instructions.append("2. Create your models in each app's models.py\n") - instructions.append("3. Run migrations: just makemigrations\n") - instructions.append("4. Apply migrations: just migrate\n") - instructions.append("5. Create views, serializers and routes(URLs) for your app(s)\n") - - console.print(Panel(instructions, title="💡 What's Next", border_style="green")) - UIFormatter.print_info("") + command = AppCommand(args) + command.execute() diff --git a/djinit/commands/secret.py b/djinit/commands/secret.py new file mode 100644 index 0000000..9c26b44 --- /dev/null +++ b/djinit/commands/secret.py @@ -0,0 +1,34 @@ +""" +Secret key generation command. +""" + +from rich.panel import Panel +from rich.text import Text + +from djinit.core.base import BaseCommand +from djinit.ui.console import UIColors, UIFormatter, console +from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys + + +class SecretCommand(BaseCommand): + """Command to generate Django secret keys.""" + + def execute(self) -> None: + count = getattr(self.args, "count", 3) + length = getattr(self.args, "length", 50) + + keys = generate_multiple_keys(count, length) + UIFormatter.print_info("") + display_secret_keys(keys) + + instructions = Text() + instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT) + instructions.append("1. Copy the appropriate secret key for your environment\n") + instructions.append("2. Add it to your .env file:\n") + instructions.append(" SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE) + instructions.append("3. Or set it as an environment variable:\n") + instructions.append(" export SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE) + instructions.append("4. Never commit secret keys to version control!\n", style=UIColors.WARNING) + + console.print(Panel(instructions, title="💡 How to Use", border_style="blue")) + console.print() -- 2.43.0 From 744f65efc7094c9466a9d6ab5a32b71de83f3d44 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 02:48:21 +0545 Subject: [PATCH 12/24] refactor(djinit/ui): use CommonUtils.get_package_name instead of get_package_name - Refactor imports to use CommonUtils for resolving package names - Update calls in `InputCollector` and `get_user_input` to use `CommonUtils.get_package_name` - No functional change; improve code consistency and future extensibility --- djinit/ui/input.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/djinit/ui/input.py b/djinit/ui/input.py index e0ff02d..7482a28 100644 --- a/djinit/ui/input.py +++ b/djinit/ui/input.py @@ -11,7 +11,7 @@ import questionary from djinit.core.types import ProjectMetadata from djinit.ui.console import UIColors, UIFormatter, console -from djinit.utils.common import get_package_name +from djinit.utils.common import CommonUtils from djinit.utils.validators import validate_app_name, validate_project_name @@ -178,7 +178,7 @@ class InputCollector: app_names: list[str] = [] # Default package_name to "backend" if project_dir is "." or empty - package_name = get_package_name(project_dir) + package_name = CommonUtils.get_package_name(project_dir) project_module_name = "config" if options["predefined"] else "core" if options["unified"] else None @@ -264,7 +264,7 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: # Generate Metadata if use_standard: - package_name = get_package_name(project_dir) + package_name = CommonUtils.get_package_name(project_dir) metadata = ProjectMetadata( package_name=package_name, use_github_actions=use_github, -- 2.43.0 From 85981115ce2eb1b2914b52bdc9b8841bc368dd5a Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 02:48:26 +0545 Subject: [PATCH 13/24] refactor(djinit/utils): centralize utility functions into CommonUtils class - Enhancements: - Moved file and directory creation helpers into CommonUtils class - Updated all call sites to use CommonUtils methods - Removed unused standalone utility functions and imports - Preserved existing behavior without functional changes --- djinit/utils/common.py | 567 +++++++++++++++++++++-------------------- djinit/utils/django.py | 37 ++- 2 files changed, 307 insertions(+), 297 deletions(-) diff --git a/djinit/utils/common.py b/djinit/utils/common.py index 893194b..3a1e153 100644 --- a/djinit/utils/common.py +++ b/djinit/utils/common.py @@ -8,322 +8,333 @@ import subprocess import sys from contextlib import contextmanager +from djinit.core.base import BaseUtils 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) +class CommonUtils(BaseUtils): + @staticmethod + def format_file(filename: str) -> None: + """Format Python file using Ruff formatter.""" + subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True) + @staticmethod + def create_file_with_content( + filename: str, content: str, success_message: str, should_format: bool = False + ) -> None: + from djinit.utils.exceptions import FileError -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 - try: - with open(filename, "w") as file: - file.write(content) - except OSError as e: - raise FileError(f"Failed to write file: {filename}", details=str(e)) from e + if should_format: + CommonUtils.format_file(filename) - if should_format: - format_file(filename) + UIFormatter.print_success(success_message) - UIFormatter.print_success(success_message) + @staticmethod + def create_file_from_template( + file_path: str, + template_path: str, + context: dict, + success_message: str, + should_format: bool = True, + ) -> None: + try: + content = template_engine.render_template(template_path, context) + except Exception as e: + from djinit.utils.exceptions import TemplateError + raise TemplateError(f"Failed to render template: {template_path}", details=str(e)) from e -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 + CommonUtils.create_file_with_content(file_path, content, success_message, should_format=should_format) - raise TemplateError(f"Failed to render template: {template_path}", details=str(e)) from e + @staticmethod + def create_init_file(directory: str, success_message: str) -> None: + init_path = os.path.join(directory, "__init__.py") + CommonUtils.create_file_from_template( + init_path, "project/init.py-tpl", {}, success_message, should_format=False + ) - create_file_with_content(file_path, content, success_message, should_format=should_format) + @staticmethod + def create_directory_with_init(dir_path: str, message: str) -> None: + """Create directory and __init__.py file.""" + os.makedirs(dir_path, exist_ok=True) + CommonUtils.create_init_file(dir_path, message) + @staticmethod + def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None: + """Create multiple files from templates. -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.py-tpl", {}, 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 + 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: - new_lines.append(line) + # Support (filename, template_path) format with empty context + filename, template_path = file_spec + context = {} + filepath = os.path.join(base_dir, filename) + message = f"Created {prefix}{filename}" if prefix else f"Created {filename}" + CommonUtils.create_file_from_template( + filepath, template_path, context, message, should_format=should_format + ) - 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) + @staticmethod + def get_package_name(project_dir: str) -> str: + """Get package name, defaulting to 'backend' if project_dir is '.' or empty.""" + return "backend" if project_dir == "." or not project_dir else project_dir - 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 + @staticmethod + @contextmanager + def change_cwd(path: str): + original_cwd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(original_cwd) - else: - new_lines.append(line) - - if not apps_added: - UIFormatter.print_error("Could not find or update USER_DEFINED_APPS section in base.py") + @staticmethod + def _parse_app_entry_from_line(line: str) -> str | None: + """Extract app name from a line in USER_DEFINED_APPS section.""" + line_stripped = line.strip().strip(",").strip() + for quote in ('"', "'"): + if line_stripped.startswith(quote) and line_stripped.endswith(quote): + return line_stripped.strip(quote) return None - return "\n".join(new_lines) + @staticmethod + def _is_user_apps_section_start(line: str) -> bool: + """Check if line starts USER_DEFINED_APPS section.""" + return "USER_DEFINED_APPS" in line and "=" in line + @staticmethod + def _iterate_user_apps_lines(content: str): + """Generator that yields lines within USER_DEFINED_APPS section.""" + lines = content.split("\n") + in_user_apps = False -def replace_app_in_user_defined_apps(content: str, old_app: str, new_app: str) -> str | None: - """Replace an app entry in USER_DEFINED_APPS with a new one.""" - lines = content.split("\n") - new_lines = [] - replaced = False + for line in lines: + if CommonUtils._is_user_apps_section_start(line): + in_user_apps = True + elif in_user_apps and line.strip() == "]": + break + elif in_user_apps: + yield line - for line in lines: - if not replaced and _parse_app_entry_from_line(line) == old_app: - # Try to preserve indentation - indent_idx = 0 - for char in line: - if char.isspace(): - indent_idx += 1 + @staticmethod + def extract_existing_apps(content: str) -> set: + existing_apps = set() + for line in CommonUtils._iterate_user_apps_lines(content): + app_name = CommonUtils._parse_app_entry_from_line(line) + if app_name: + existing_apps.add(app_name) + return existing_apps + + @staticmethod + def format_app_entries(apps: list) -> list: + return [f' "{app}",' for app in apps] + + @staticmethod + def is_app_section_start(line: str) -> bool: + return "=" in line and any( + keyword in line for keyword in ("BUILT_IN_APPS", "THIRD_PARTY_APPS", "INSTALLED_APPS") + ) + + @staticmethod + def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool: + for next_idx in range(current_idx + 1, min(current_idx + 3, len(lines))): + next_line = lines[next_idx].strip() + if next_line and "BUILT_IN_APPS" in next_line: + return True + return True # Default to True if we're inside USER_DEFINED_APPS + + @staticmethod + def split_bracket_line(line: str, apps_to_add: list) -> list: + bracket_pos = line.rfind("]") + before_bracket = line[:bracket_pos].rstrip() if bracket_pos > 0 else line.rstrip() + after_bracket = line[bracket_pos:] if bracket_pos > 0 else "]" + + result = [before_bracket] + result.extend(CommonUtils.format_app_entries(apps_to_add)) + result.append(after_bracket) + return result + + @staticmethod + def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str: + lines = content.split("\n") + new_lines = [] + in_user_apps = False + apps_added = False + + for i, line in enumerate(lines): + if CommonUtils._is_user_apps_section_start(line) and not apps_added: + in_user_apps = True + + if "]" in line: + new_lines.extend(CommonUtils.split_bracket_line(line, apps_to_add)) + apps_added = True + in_user_apps = False else: - break - indent = line[:indent_idx] if indent_idx > 0 else " " - - new_lines.append(f'{indent}"{new_app}",') - replaced = True - else: - new_lines.append(line) + new_lines.append(line) + + elif in_user_apps and line.strip() == "]" and not apps_added: + if CommonUtils.is_user_apps_closing_bracket(lines, i): + new_lines.extend(CommonUtils.format_app_entries(apps_to_add)) + new_lines.append(line) + apps_added = True + in_user_apps = False + else: + new_lines.append(line) + + elif in_user_apps and CommonUtils.is_app_section_start(line) and not apps_added: + new_lines.extend(CommonUtils.format_app_entries(apps_to_add)) + new_lines.append("]") + new_lines.append(line) + apps_added = True + in_user_apps = False + + else: + new_lines.append(line) + + if not apps_added: + UIFormatter.print_error("Could not find or update USER_DEFINED_APPS section in base.py") + return None - if replaced: return "\n".join(new_lines) - return None + @staticmethod + def replace_app_in_user_defined_apps(content: str, old_app: str, new_app: str) -> str | None: + """Replace an app entry in USER_DEFINED_APPS with a new one.""" + lines = content.split("\n") + new_lines = [] + replaced = False -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] + for line in lines: + if not replaced and CommonUtils._parse_app_entry_from_line(line) == old_app: + # Try to preserve indentation + indent_idx = 0 + for char in line: + if char.isspace(): + indent_idx += 1 + else: + break + indent = line[:indent_idx] if indent_idx > 0 else " " + new_lines.append(f'{indent}"{new_app}",') + replaced = True + else: + new_lines.append(line) -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): + if replaced: + return "\n".join(new_lines) return None - with open(base_settings_path) as f: - return f.read() + @staticmethod + def calculate_app_module_paths(app_names: list, metadata: dict) -> list: + nested = bool(metadata.get("nested_apps")) + nested_dir_name = metadata.get("nested_dir") if nested else None + return [CommonUtils.calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names] + @staticmethod + def calculate_app_module_path(app_name: str, nested: bool, nested_dir: str | None) -> str: + return f"{nested_dir}.{app_name}" if nested and nested_dir else app_name -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() + @staticmethod + def is_django_project(directory: str = None) -> bool: + if directory is None: + directory = os.getcwd() + manage_py_path = os.path.join(directory, "manage.py") + return os.path.exists(manage_py_path) + + @staticmethod + def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]: + if search_dir is None: + search_dir = os.getcwd() + + for item in os.listdir(search_dir): + if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__": + candidate = os.path.join(search_dir, item) + base_py = os.path.join(candidate, "settings", "base.py") + if os.path.exists(base_py): + return candidate, base_py + return None, None + + @staticmethod + def find_settings_path(search_dir: str = None) -> str | None: + project_dir, settings_base_path = CommonUtils.find_project_dir(search_dir) + if settings_base_path: + return os.path.dirname(settings_base_path) + return None + + @staticmethod + def get_base_settings_path(project_root: str, project_name: str) -> str: + settings_dir = os.path.join(project_root, project_name, "settings") + return os.path.join(settings_dir, "base.py") + + @staticmethod + def read_base_settings(project_root: str, project_name: str) -> str | None: + base_settings_path = CommonUtils.get_base_settings_path(project_root, project_name) + if not os.path.exists(base_settings_path): + return None + + with open(base_settings_path) as f: + return f.read() + + @staticmethod + def detect_nested_structure_from_settings( + base_settings_path: str, search_dir: str = None + ) -> tuple[bool, str | None, str]: + if search_dir is None: + search_dir = os.getcwd() + + if not os.path.exists(base_settings_path): + return False, None, search_dir + + try: + with open(base_settings_path) as f: + base_content = f.read() + + for line in CommonUtils._iterate_user_apps_lines(base_content): + app_label = CommonUtils._parse_app_entry_from_line(line) + if app_label and "." in app_label: + nested_dir_name = app_label.split(".", 1)[0] + nested_path = os.path.join(search_dir, nested_dir_name) + if os.path.isdir(nested_path): + return True, nested_dir_name, nested_path + except Exception: + pass - 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() + @staticmethod + def get_djinit_config(project_root: str = None) -> dict | None: + """Read .djinit configuration file.""" + import json - 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 + if project_root is None: + project_root = os.getcwd() - return False, None, search_dir + config_path = os.path.join(project_root, ".djinit") + if not os.path.exists(config_path): + # Try finding it in parent directories + parent = os.path.dirname(project_root) + if parent and parent != project_root: + return CommonUtils.get_djinit_config(parent) + return None - -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 + try: + with open(config_path) as f: + return json.load(f) + except Exception: + return None diff --git a/djinit/utils/django.py b/djinit/utils/django.py index cecbf20..7eeabba 100644 --- a/djinit/utils/django.py +++ b/djinit/utils/django.py @@ -7,12 +7,7 @@ This module provides functions to create Django projects and apps without requir 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, -) +from djinit.utils.common import CommonUtils class DjangoHelper: @@ -24,7 +19,7 @@ class DjangoHelper: os.makedirs(directory, exist_ok=True) manage_py_path = os.path.join(directory, "manage.py") - create_file_from_template( + CommonUtils.create_file_from_template( manage_py_path, "project/manage.py-tpl", {"project_name": project_name}, "Created manage.py" ) os.chmod(manage_py_path, 0o755) @@ -33,10 +28,10 @@ class DjangoHelper: return project_config_dir = os.path.join(directory, project_name) - create_directory_with_init(project_config_dir, f"Created {project_name}/__init__.py") + CommonUtils.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") + CommonUtils.create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py") base_context = {"project_name": project_name, "app_names": []} settings_files = [ @@ -44,15 +39,19 @@ class DjangoHelper: ("development.py", "config/settings/development.py-tpl", {}), ("production.py", "config/settings/production.py-tpl", {}), ] - create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/") + CommonUtils.create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/") - urls_context = {"url_type": "project", "project_name": project_name, "django_version": DjangoHelper.DJANGO_VERSION} + urls_context = { + "url_type": "project", + "project_name": project_name, + "django_version": DjangoHelper.DJANGO_VERSION, + } project_files = [ ("urls.py", "config/urls.py-tpl", urls_context), ("wsgi.py", "config/wsgi.py-tpl", {}), ("asgi.py", "config/asgi.py-tpl", {}), ] - create_files_from_templates(project_config_dir, project_files, f"{project_name}/") + CommonUtils.create_files_from_templates(project_config_dir, project_files, f"{project_name}/") except Exception as e: from djinit.utils.exceptions import DjinitError @@ -66,20 +65,20 @@ class DjangoHelper: os.makedirs(app_dir, exist_ok=True) # Prepare context - app_config_name = app_name.title().replace('_', '') + app_config_name = app_name.title().replace("_", "") context = { "app_name": app_name, "app_config_name": app_config_name, - "django_version": DjangoHelper.DJANGO_VERSION + "django_version": DjangoHelper.DJANGO_VERSION, } - + # Use provided app_module or default to app_name (path calculation handled by caller) if not app_module: app_module = app_name - + context["app_module"] = app_module - create_init_file(app_dir, f"Created {app_name}/__init__.py") + CommonUtils.create_init_file(app_dir, f"Created {app_name}/__init__.py") app_files = [ ("apps.py", "components/apps.py-tpl", context), @@ -92,10 +91,10 @@ class DjangoHelper: ("tests.py", "components/tests.py-tpl", context), ] - create_files_from_templates(app_dir, app_files, f"{app_name}/") + CommonUtils.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") + CommonUtils.create_directory_with_init(migrations_dir, f"Created {app_name}/migrations/__init__.py") except Exception as e: from djinit.utils.exceptions import DjinitError -- 2.43.0 From 0cff748d5e5c49e31f636f6c4e919f33f0a18eb0 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 02:48:32 +0545 Subject: [PATCH 14/24] feat(djinit/templates): Refine template placeholders for production and environment configuration files Added conditional comment handling for database settings in production template, introduced DATABASE_URL generation in env_sample, and corrected readme structure rendering. New Features - Added conditional comment handling for `use_database_url` and `database_type` in production settings. - Added `DATABASE_URL` template logic for MySQL and PostgreSQL in env_sample. - Updated `readme.md` template to properly render unified_structure sections. Enhancements - Ensured comment syntax consistency across all template files. - Streamlined placeholder usage for clearer project scaffolding. Bug Fixes - Removed stray formatting characters from comment markers. --- djinit/templates/config/settings/production.py-tpl | 4 ++-- djinit/templates/project/env_sample-tpl | 4 ++-- djinit/templates/project/readme.md-tpl | 4 ++-- djinit/templates/project/requirements-tpl | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/djinit/templates/config/settings/production.py-tpl b/djinit/templates/config/settings/production.py-tpl index 72d0c42..7d58363 100644 --- a/djinit/templates/config/settings/production.py-tpl +++ b/djinit/templates/config/settings/production.py-tpl @@ -8,7 +8,7 @@ SECRET_KEY = env('SECRET_KEY') ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) # Database -# @IF use_database_url +# @IF use_database_url # Using dj_database_url (recommended for production) DATABASES = { 'default': env.db('DATABASE_URL') @@ -17,7 +17,7 @@ DATABASES = { # Using individual database parameters DATABASES = { 'default': { - # @IF database_type == 'mysql' + # @IF database_type == 'mysql' 'ENGINE': 'django.db.backends.mysql', 'PORT': env('DB_PORT', default='3306'), # @ELSE diff --git a/djinit/templates/project/env_sample-tpl b/djinit/templates/project/env_sample-tpl index 65d6da7..382da28 100644 --- a/djinit/templates/project/env_sample-tpl +++ b/djinit/templates/project/env_sample-tpl @@ -3,9 +3,9 @@ DJANGO_SETTINGS_MODULE=[[ project_name ]].settings.development SECRET_KEY= your-secret-key ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com -# @IF use_database_url +# @IF use_database_url # Database URL (recommended for production) -# @IF database_type == 'mysql' +# @IF database_type == 'mysql' DATABASE_URL=mysql://user:password@localhost:3306/[[ project_name ]] # @ELSE DATABASE_URL=postgres://user:password@localhost:5432/[[ project_name ]] diff --git a/djinit/templates/project/readme.md-tpl b/djinit/templates/project/readme.md-tpl index c107ade..9c28632 100644 --- a/djinit/templates/project/readme.md-tpl +++ b/djinit/templates/project/readme.md-tpl @@ -71,7 +71,7 @@ Run `just` to see all available commands, including: ## Project Structure ``` -# @IF predefined_structure +# @IF predefined_structure [[ project_name ]]/ ├── [[ module_name ]]/ │ ├── settings/ @@ -118,7 +118,7 @@ Run `just` to see all available commands, including: ├── Procfile ├── runtime.txt └── README.md -# @ELSEIF unified_structure +# @ELSEIF unified_structure [[ project_name ]]/ ├── apps/ │ ├── admin/ diff --git a/djinit/templates/project/requirements-tpl b/djinit/templates/project/requirements-tpl index 43aee21..cb48ff9 100644 --- a/djinit/templates/project/requirements-tpl +++ b/djinit/templates/project/requirements-tpl @@ -10,7 +10,7 @@ djangorestframework_simplejwt drf-spectacular django-cors-headers whitenoise -# @IF database_type == 'mysql' +# @IF database_type == 'mysql' mysqlclient # @ELSE psycopg[binary] -- 2.43.0 From a293ad9771adc464d13a9527dad79a03cd816d59 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 22:23:15 +0545 Subject: [PATCH 15/24] update --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2b5243c..ef0d5bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ #--------------------------------------------------# # The following was generated by djinit: # #--------------------------------------------------# +AGENTS.md # Python *.py[cod] -- 2.43.0 From a3d20860b6a148364ea4ab11952784dc411510e1 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 22:23:35 +0545 Subject: [PATCH 16/24] refactor(root): migrate project to src layout and update CLI entry points New Features - Add Typer dependency. Enhancements - Move packages to src/ layout and update build configuration. - Change console scripts to use djinit.cli:app. - Exclude django-new from ruff checks. - Raise minimum Python version to >=3.10. Bug Fixes - none --- pyproject.toml | 7 ++++--- ruff.toml | 1 + setup.py | 12 ++++++------ uv.lock | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b27215..d93267d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["djinit"] +packages = ["src/djinit"] [project] name = "djinitx" @@ -49,14 +49,15 @@ classifiers=[ dependencies = [ "rich>=14.2.0", "questionary>=2.0.1", + "typer>=0.21.1", ] [project.urls] Homepage = "https://github.com/S4NKALP/djinit" [project.scripts] -djinit = "djinit.generator:main" -dj = "djinit.generator:main" +djinit = "djinit.cli:app" +dj = "djinit.cli:app" [dependency-groups] dev = [ diff --git a/ruff.toml b/ruff.toml index 9adb00f..c693f21 100644 --- a/ruff.toml +++ b/ruff.toml @@ -2,6 +2,7 @@ line-length = 120 target-version = "py38" +exclude = ["django-new"] [lint] select = [ diff --git a/setup.py b/setup.py index 3127149..49fb89f 100644 --- a/setup.py +++ b/setup.py @@ -9,18 +9,18 @@ setup( author="Sankalp Tharu", author_email="sankalptharu50028@gmail.com", url="https://github.com/S4NKALP/djinit", - packages=find_packages(include=["djinit", "djinit.*"]), + packages=find_packages(where="src"), + package_dir={"": "src"}, include_package_data=True, install_requires=[ - "ruff>=0.14.2", - "click>=8.3.0", "rich>=14.2.0", "questionary>=2.0.1", + "typer>=0.21.1", ], entry_points={ "console_scripts": [ - "djinit=djinit.generator:main", - "dj=djinit.generator:main", + "djinit=djinit.cli:app", + "dj=djinit.cli:app", ] }, classifiers=[ @@ -54,7 +54,7 @@ setup( "django project", "django starter", ], - python_requires=">=3.13", + python_requires=">=3.10", project_urls={ "Bug Reports": "https://github.com/S4NKALP/djinit/issues", "Source": "https://github.com/S4NKALP/djinit", diff --git a/uv.lock b/uv.lock index 8060923..4d6b15b 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -27,6 +48,7 @@ source = { editable = "." } dependencies = [ { name = "questionary" }, { name = "rich" }, + { name = "typer" }, ] [package.dev-dependencies] @@ -39,6 +61,7 @@ dev = [ requires-dist = [ { name = "questionary", specifier = ">=2.0.1" }, { name = "rich", specifier = ">=14.2.0" }, + { name = "typer", specifier = ">=0.21.1" }, ] [package.metadata.requires-dev] @@ -256,6 +279,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "typer" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" -- 2.43.0 From 7d83f41c4871fedc69403cb90564f0212da87600 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 22:23:40 +0545 Subject: [PATCH 17/24] refactor(djinit/commands): remove unused command modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These files (app.py, handler.py, secret.py) were no longer referenced by the active CLI flow and their removal eliminates dead code and import side‑effects. New Features - Enhancements - Streamlined command dispatch by dropping redundant handlers. - Reduced module surface area, improving maintainability. Bug Fixes - Fixed import‑induced crashes when the CLI starts without these optional sub‑commands. --- djinit/commands/__init__.py | 0 djinit/commands/app.py | 64 ------------------------------------- djinit/commands/handler.py | 49 ---------------------------- djinit/commands/secret.py | 34 -------------------- 4 files changed, 147 deletions(-) delete mode 100644 djinit/commands/__init__.py delete mode 100644 djinit/commands/app.py delete mode 100644 djinit/commands/handler.py delete mode 100644 djinit/commands/secret.py diff --git a/djinit/commands/__init__.py b/djinit/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/djinit/commands/app.py b/djinit/commands/app.py deleted file mode 100644 index 875d18e..0000000 --- a/djinit/commands/app.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -App creation command. -""" - -import sys - -from rich.panel import Panel -from rich.text import Text - -from djinit.core.base import BaseCommand -from djinit.services.app import AppManager -from djinit.ui.console import UIColors, UIFormatter, console -from djinit.utils.validators import validate_app_name - - -class AppCommand(BaseCommand): - """Command to create Django apps.""" - - def execute(self) -> None: - UIFormatter.print_info("") - UIFormatter.print_header("Django App Creation") - UIFormatter.print_info("") - - raw_tokens = self.args.app_name if isinstance(self.args.app_name, list) else [self.args.app_name] - - # Flatten and clean tokens - app_names = [part.strip() for token in raw_tokens if token for part in str(token).split(",") if part.strip()] - - if not app_names: - UIFormatter.print_error("App name cannot be empty") - sys.exit(1) - - # Deduplicate while preserving order - deduped_names = list(dict.fromkeys(app_names)) - - for name in deduped_names: - is_valid, error_msg = validate_app_name(name) - if not is_valid: - UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}") - sys.exit(1) - - any_failure = False - for name in deduped_names: - app_manager = AppManager(name) - success = app_manager.create_app() - if not success: - UIFormatter.print_error(f"Failed to create Django app '{name}'") - any_failure = True - break - UIFormatter.print_success(f"Django app '{name}' created successfully!") - - if any_failure: - sys.exit(1) - - instructions = Text() - instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT) - instructions.append("1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n") - instructions.append("2. Create your models in each app's models.py\n") - instructions.append("3. Run migrations: just makemigrations\n") - instructions.append("4. Apply migrations: just migrate\n") - instructions.append("5. Create views, serializers and routes(URLs) for your app(s)\n") - - console.print(Panel(instructions, title="💡 What's Next", border_style="green")) - UIFormatter.print_info("") diff --git a/djinit/commands/handler.py b/djinit/commands/handler.py deleted file mode 100644 index c30a767..0000000 --- a/djinit/commands/handler.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Command handlers for Django project setup CLI. -Handles different commands like secret key generation and app creation. -""" - -import argparse - - -def parse_arguments() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Django project setup tool", prog="django-init") - - subparsers = parser.add_subparsers(dest="command", help="Available commands") - - setup_parser = subparsers.add_parser("setup", help="Create a new Django project") - setup_parser.add_argument("--project", help="Django project name") - setup_parser.add_argument("--app", help="Django app name") - - secret_parser = subparsers.add_parser("secret", help="Generate Django secret keys") - secret_parser.add_argument("--count", type=int, default=3, help="Number of keys to generate") - secret_parser.add_argument("--length", type=int, default=50, help="Length of each secret key") - - app_parser = subparsers.add_parser("app", help="Create one or more Django apps (comma or space separated)") - app_parser.add_argument( - "app_name", - nargs="+", - help=( - "App name(s) or comma-separated list. Examples: \n" - " djinit app users \n" - " djinit app users,products,orders \n" - " djinit app users products orders \n" - " djinit app users, products, orders" - ), - ) - - return parser.parse_args() - - -def handle_secret_command(args: argparse.Namespace) -> None: - from djinit.commands.secret import SecretCommand - - command = SecretCommand(args) - command.execute() - - -def handle_app_command(args: argparse.Namespace) -> None: - from djinit.commands.app import AppCommand - - command = AppCommand(args) - command.execute() diff --git a/djinit/commands/secret.py b/djinit/commands/secret.py deleted file mode 100644 index 9c26b44..0000000 --- a/djinit/commands/secret.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Secret key generation command. -""" - -from rich.panel import Panel -from rich.text import Text - -from djinit.core.base import BaseCommand -from djinit.ui.console import UIColors, UIFormatter, console -from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys - - -class SecretCommand(BaseCommand): - """Command to generate Django secret keys.""" - - def execute(self) -> None: - count = getattr(self.args, "count", 3) - length = getattr(self.args, "length", 50) - - keys = generate_multiple_keys(count, length) - UIFormatter.print_info("") - display_secret_keys(keys) - - instructions = Text() - instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT) - instructions.append("1. Copy the appropriate secret key for your environment\n") - instructions.append("2. Add it to your .env file:\n") - instructions.append(" SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE) - instructions.append("3. Or set it as an environment variable:\n") - instructions.append(" export SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE) - instructions.append("4. Never commit secret keys to version control!\n", style=UIColors.WARNING) - - console.print(Panel(instructions, title="💡 How to Use", border_style="blue")) - console.print() -- 2.43.0 From 866ddd010547e54d78f254d059e45eea7873054f Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 22:23:46 +0545 Subject: [PATCH 18/24] refactor(djinit): Remove CLI and utility modules The deletion of djinit's internal modules (cli, utils, services) eliminates obsolete code and prepares the repository for a streamlined Django project initialization approach. - New Features: - Enhancements: - Bug Fixes: --- djinit/__init__.py | 0 djinit/cli.py | 120 --------- djinit/core/__init__.py | 0 djinit/core/base.py | 52 ---- djinit/core/config.py | 14 -- djinit/core/parser.py | 215 ---------------- djinit/core/types.py | 48 ---- djinit/generator.py | 63 ----- djinit/services/__init__.py | 0 djinit/services/app.py | 302 ---------------------- djinit/services/files.py | 469 ----------------------------------- djinit/services/project.py | 198 --------------- djinit/services/templates.py | 89 ------- djinit/ui/__init__.py | 0 djinit/ui/console.py | 206 --------------- djinit/ui/input.py | 337 ------------------------- djinit/utils/__init__.py | 0 djinit/utils/common.py | 340 ------------------------- djinit/utils/django.py | 102 -------- djinit/utils/exceptions.py | 30 --- djinit/utils/secretkey.py | 39 --- djinit/utils/validators.py | 47 ---- 22 files changed, 2671 deletions(-) delete mode 100644 djinit/__init__.py delete mode 100644 djinit/cli.py delete mode 100644 djinit/core/__init__.py delete mode 100644 djinit/core/base.py delete mode 100644 djinit/core/config.py delete mode 100644 djinit/core/parser.py delete mode 100644 djinit/core/types.py delete mode 100644 djinit/generator.py delete mode 100644 djinit/services/__init__.py delete mode 100644 djinit/services/app.py delete mode 100644 djinit/services/files.py delete mode 100644 djinit/services/project.py delete mode 100644 djinit/services/templates.py delete mode 100644 djinit/ui/__init__.py delete mode 100644 djinit/ui/console.py delete mode 100644 djinit/ui/input.py delete mode 100644 djinit/utils/__init__.py delete mode 100644 djinit/utils/common.py delete mode 100644 djinit/utils/django.py delete mode 100644 djinit/utils/exceptions.py delete mode 100644 djinit/utils/secretkey.py delete mode 100644 djinit/utils/validators.py diff --git a/djinit/__init__.py b/djinit/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/djinit/cli.py b/djinit/cli.py deleted file mode 100644 index 318a58d..0000000 --- a/djinit/cli.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Main CLI orchestrator for djinit. -Coordinates between different managers to create a complete Django project. -""" - -import os -from typing import Callable, List, Tuple - -from djinit.services.files import FileManager -from djinit.services.project import ProjectManager -from djinit.ui.console import UIFormatter - - -class Cli: - def __init__(self, project_dir: str, project_name: str, primary_app: str, app_names: list, metadata: dict): - self.project_dir = project_dir - self.project_name = project_name - self.primary_app = primary_app - self.app_names = app_names - self.metadata = metadata - if project_dir == ".": - self.project_root = os.getcwd() - else: - self.project_root = os.path.join(os.getcwd(), project_dir) - - self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata) - self.file_manager = FileManager(self.project_root, project_name, app_names, metadata) - - 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: - 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 - - progress, task = UIFormatter.create_live_progress(description="Setup Progress", total_steps=total_steps) - - 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}") - - except Exception as e: - success = False - UIFormatter.handle_exception(e) - - return success - - 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(self.metadata), - ] - - for step_func in utility_steps: - step_func() - - UIFormatter.print_success("Created all utility files successfully!") - - def _create_cicd_pipelines(self) -> None: - if self.metadata.get("use_github_actions", True): - self.file_manager.create_github_actions() - - if self.metadata.get("use_gitlab_ci", True): - self.file_manager.create_gitlab_ci() diff --git a/djinit/core/__init__.py b/djinit/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/djinit/core/base.py b/djinit/core/base.py deleted file mode 100644 index 452528a..0000000 --- a/djinit/core/base.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Base classes for the djinit architecture. -Defines the common interface for services, commands, and utilities. -""" - -from abc import abstractmethod -from typing import Any - - -class BaseObject: - """ - Base object for all djinit classes. - """ - - def __init__(self, *args, **kwargs): - pass - - -class BaseService(BaseObject): - """ - Base class for all services. - Services handle business logic and orchestration. - """ - - def __init__(self, project_root: str = None, *args, **kwargs): - super().__init__(*args, **kwargs) - self.project_root = project_root - - -class BaseCommand(BaseObject): - """ - Base class for all CLI commands. - Commands handle user input and invoke services. - """ - - def __init__(self, args: Any, *args_list, **kwargs): - super().__init__(*args_list, **kwargs) - self.args = args - - @abstractmethod - def execute(self) -> None: - """Execute the command.""" - pass - - -class BaseUtils(BaseObject): - """ - Base class for utility collections. - Utilities provide helper functions. - """ - - pass diff --git a/djinit/core/config.py b/djinit/core/config.py deleted file mode 100644 index 61b610e..0000000 --- a/djinit/core/config.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Core configuration and constants for djinit. -""" - -import sys - -# Django version used for project generation -DJANGO_VERSION = "6.0.1" - -# 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) diff --git a/djinit/core/parser.py b/djinit/core/parser.py deleted file mode 100644 index 5be47bf..0000000 --- a/djinit/core/parser.py +++ /dev/null @@ -1,215 +0,0 @@ -import re -from typing import Any, Dict, List - - -class InFileLogicParser: - """ - A lightweight template parser that uses comment-style markers for logic - and [[ variable ]] for substitutions. - """ - - def __init__(self, context: Dict[str, Any] = None): - self.context = context or {} - - def _get_value(self, key: str) -> str: - """ - Supports nested/dotted access like [[ user.name ]] or [[ settings["DEBUG"] ]]. - For simplicity, it uses eval in the provided context for complex expressions, - but falls back to safe dictionary lookup for simple keys. - """ - key = key.strip() - try: - # Try evaluating the expression in the context - return str(eval(key, {"__builtins__": {}}, self.context)) - except (NameError, SyntaxError, KeyError, TypeError, AttributeError): - # Fallback for common patterns or just return the key as is if it fails - return f"[[ {key} ]]" - - def render(self, template_text: str, context: Dict[str, Any] = None) -> str: - if context is not None: - self.context = context - - final_lines = [] - # Stack stores boolean results of nested IF blocks - # Each element is (current_block_result, has_any_true_branch_executed) - stack: List[List[bool]] = [] - - lines = template_text.splitlines() - i = 0 - while i < len(lines): - line = lines[i] - stripped = line.strip() - - # Handle @IF - if stripped.startswith("# @IF "): - expr = stripped[6:].strip() - try: - result = bool(eval(expr, {"__builtins__": {}}, self.context)) - except Exception: - result = False - stack.append([result, result]) - i += 1 - continue - - # Handle @ELSEIF - elif stripped.startswith("# @ELSEIF "): - if not stack: - final_lines.append(line) - i += 1 - continue - - expr = stripped[9:].strip() - current_stack = stack[-1] - - if current_stack[1]: - current_stack[0] = False - else: - try: - result = bool(eval(expr, {"__builtins__": {}}, self.context)) - except Exception: - result = False - current_stack[0] = result - if result: - current_stack[1] = True - i += 1 - continue - - # Handle @ELSE - elif stripped.startswith("# @ELSE"): - if not stack: - final_lines.append(line) - i += 1 - continue - - current_stack = stack[-1] - if current_stack[1]: - current_stack[0] = False - else: - current_stack[0] = True - current_stack[1] = True - i += 1 - continue - - # Handle @ENDIF - elif stripped.startswith("# @ENDIF"): - if stack: - stack.pop() - else: - final_lines.append(line) - i += 1 - continue - - # Handle @LOOP - elif stripped.startswith("# @LOOP "): - if stack and not all(s[0] for s in stack): - # Skip the entire loop block if inside a false conditional - loop_depth = 1 - i += 1 - while i < len(lines) and loop_depth > 0: - if lines[i].strip().startswith("# @LOOP "): - loop_depth += 1 - elif lines[i].strip().startswith("# @ENDLOOP"): - loop_depth -= 1 - i += 1 - continue - - parts = stripped[7:].strip().split(" in ") - if len(parts) == 2: - var_name = parts[0].strip() - iterable_name = parts[1].strip() - try: - iterable = eval(iterable_name, {"__builtins__": {}}, self.context) - except Exception: - iterable = [] - - # Capture loop body - loop_body = [] - i += 1 - loop_depth = 1 - while i < len(lines) and loop_depth > 0: - if lines[i].strip().startswith("# @LOOP "): - loop_depth += 1 - elif lines[i].strip().startswith("# @ENDLOOP"): - loop_depth -= 1 - - if loop_depth > 0: - loop_body.append(lines[i]) - i += 1 - - if i < len(lines): # Skip the @ENDLOOP line itself - i += 1 - - # Execute loop - if hasattr(iterable, "__iter__"): - old_val = self.context.get(var_name) - for val in iterable: - self.context[var_name] = val - rendered_body = self.render("\n".join(loop_body), self.context) - final_lines.extend(rendered_body.splitlines()) - if old_val is not None: - self.context[var_name] = old_val - else: - self.context.pop(var_name, None) - continue - else: - final_lines.append(line) - i += 1 - continue - - # Handle @ENDLOOP (should only be hit if out of sync or error) - elif stripped.startswith("# @ENDLOOP"): - i += 1 - continue - - # Check if we should skip this line based on conditional stack - if stack and not all(s[0] for s in stack): - i += 1 - continue - - # Variable substitution - rendered_line = line - matches = re.findall(r"\[\[\s*(.*?)\s*\]\]", rendered_line) - for match in matches: - value = self._get_value(match) - rendered_line = rendered_line.replace(f"[[ {match} ]]", value) - rendered_line = rendered_line.replace(f"[[{match}]]", value) - - # In-line @IF support - # Case 1: content # @IF cond - # Case 2: # @IF cond content - if "# @IF " in rendered_line: - parts = rendered_line.split("# @IF ", 1) - pre_content = parts[0].rstrip() - rest = parts[1].strip() - - # Split rest into expression and post-content (if any) - # This is tricky because the expression might have spaces. - # If there's an # @ENDIF on the same line, use it. - post_content = "" - expr = rest - if " # @ENDIF" in rest: - expr_parts = rest.split(" # @ENDIF", 1) - expr = expr_parts[0].strip() - post_content = expr_parts[1].lstrip() - elif "# @ENDIF" in rest: - expr_parts = rest.split("# @ENDIF", 1) - expr = expr_parts[0].strip() - post_content = expr_parts[1].lstrip() - - try: - if bool(eval(expr, {"__builtins__": {}}, self.context)): - # If Case 1, we want pre_content. If Case 2, we want post_content. - # Usually, if pre_content is empty (or just whitespace), it's Case 2. - if pre_content: - final_lines.append(pre_content + post_content) - else: - final_lines.append(post_content) - except Exception: - pass - i += 1 - continue - - final_lines.append(rendered_line) - i += 1 - - return "\n".join(final_lines) diff --git a/djinit/core/types.py b/djinit/core/types.py deleted file mode 100644 index a70454c..0000000 --- a/djinit/core/types.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -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()) diff --git a/djinit/generator.py b/djinit/generator.py deleted file mode 100644 index 660746f..0000000 --- a/djinit/generator.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Main script entry point for djinit. -Handles user input validation and orchestrates the setup process. -""" - -import os -import subprocess -import sys - -from djinit.cli import Cli -from djinit.commands.app import AppCommand -from djinit.commands.handler import parse_arguments -from djinit.commands.secret import SecretCommand -from djinit.ui.console import UIFormatter, console -from djinit.ui.input import confirm_setup, get_user_input - - -def clear_screen() -> None: - try: - if os.name == "nt": - subprocess.run("cls", shell=True, check=True) - else: - subprocess.run("clear", shell=True, check=True) - except subprocess.CalledProcessError: - console.print("\n" * 50) - - -def main() -> None: - args = parse_arguments() - - if args.command == "secret": - SecretCommand(args).execute() - return - - if args.command == "app": - AppCommand(args).execute() - return - - clear_screen() - - console.print() - console.print(UIFormatter.create_welcome_panel()) - console.print() - - project_dir, project_name, primary_app, app_names, metadata = get_user_input() - - if not confirm_setup(project_dir, project_name, app_names, metadata): - UIFormatter.print_info("Setup cancelled by user.") - return - - console.print() - UIFormatter.print_info("Starting Django project setup...") - console.print() - - django_cli = Cli(project_dir, project_name, primary_app, app_names, metadata) - success = django_cli.run_setup() - - UIFormatter.create_summary_panel(project_dir, project_name, app_names, True, metadata=metadata) - sys.exit(0 if success else 1) - - -if __name__ == "__main__": - main() diff --git a/djinit/services/__init__.py b/djinit/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/djinit/services/app.py b/djinit/services/app.py deleted file mode 100644 index 66a69ab..0000000 --- a/djinit/services/app.py +++ /dev/null @@ -1,302 +0,0 @@ -""" -App management for djinit. -Handles creation of Django apps and updating settings. -""" - -import os -from typing import Optional - -from djinit.core.base import BaseService -from djinit.ui.console import UIFormatter -from djinit.utils.common import CommonUtils -from djinit.utils.django import DjangoHelper - - -class AppManager(BaseService): - def __init__(self, app_name: str): - super().__init__() - self.app_name = app_name - self.current_dir = os.getcwd() - self.manage_py_path = os.path.join(self.current_dir, "manage.py") - self.config = CommonUtils.get_djinit_config(self.current_dir) - self._project_structure_cache = None - - def create_app(self) -> bool: - if not self._is_django_project(): - UIFormatter.print_error( - "Not in a Django project directory. Please run this command from your Django project root." - ) - return False - - 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 CommonUtils.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")) - - is_nested, nested_dir, apps_base_dir = self._get_project_structure() - - # Calculate app_module based on project structure - if is_nested and nested_dir: - # Normalize nested_dir (replace / with . for module path) - module_base = nested_dir.replace(os.path.sep, ".") - app_module = f"{module_base}.{self.app_name}" - else: - app_module = self.app_name - - success = DjangoHelper.startapp(self.app_name, apps_base_dir, app_module) - if success: - UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}") - else: - UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'") - return success - - def _add_to_installed_apps(self) -> bool: - settings_path = CommonUtils.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 = CommonUtils.calculate_app_module_path(self.app_name, is_nested, nested_dir) - - # Use full AppConfig path to properly support nested apps - # e.g. apps.users -> apps.users.apps.UsersConfig - app_config_name = self.app_name.title().replace("_", "") - full_app_config = f"{app_module_path}.apps.{app_config_name}Config" - - # We'll check if either the module path or the full config path exists - # but we'll prefer adding the full config path - - existing_apps = CommonUtils.extract_existing_apps(content) - - # Check if full config is already there - if full_app_config in existing_apps: - UIFormatter.print_success(f"App '{full_app_config}' already configured in USER_DEFINED_APPS") - return True - - # Check if legacy module path is there, if so, upgrade it - if app_module_path in existing_apps: - UIFormatter.print_info(f"Upgrading app configuration from '{app_module_path}' to '{full_app_config}'...") - updated_content = CommonUtils.replace_app_in_user_defined_apps(content, app_module_path, full_app_config) - - if updated_content: - with open(base_settings_path, "w") as f: - f.write(updated_content) - UIFormatter.print_success(f"Upgraded '{app_module_path}' to '{full_app_config}' in USER_DEFINED_APPS") - return True - else: - UIFormatter.print_error(f"Failed to upgrade app '{app_module_path}'") - return False - - if "USER_DEFINED_APPS" not in content: - UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py") - return False - - updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, [full_app_config]) - if not updated_content: - return False - - with open(base_settings_path, "w") as f: - f.write(updated_content) - - UIFormatter.print_success(f"Added '{full_app_config}' to USER_DEFINED_APPS in base.py") - return True - - def _get_project_structure(self) -> tuple[bool, Optional[str], str]: - """Get project structure with caching to avoid repeated detection.""" - if self._project_structure_cache is None: - if self.config: - apps_config = self.config.get("apps", {}) - nested = apps_config.get("nested", False) - nested_dir = apps_config.get("nested_dir") - - # Determine base dir for apps - if nested and nested_dir: - apps_base_dir = os.path.join(self.current_dir, nested_dir) - else: - apps_base_dir = self.current_dir - - self._project_structure_cache = (nested, nested_dir, apps_base_dir) - else: - # Fallback to old detection method - project_dir, settings_base_path = CommonUtils.find_project_dir(self.current_dir) - - if project_dir is None: - self._project_structure_cache = (False, None, self.current_dir) - else: - self._project_structure_cache = CommonUtils.detect_nested_structure_from_settings( - settings_base_path, self.current_dir - ) - return self._project_structure_cache - - def _is_predefined_structure(self) -> bool: - if self.config: - return self.config.get("structure", {}).get("predefined", False) - - apps_dir = os.path.join(self.current_dir, "apps") - api_dir = os.path.join(self.current_dir, "api") - return os.path.isdir(apps_dir) and os.path.isdir(api_dir) - - def _is_restricted_structure(self) -> bool: - """Check if the project structure is Unified or Single Folder.""" - if self.config: - structure = self.config.get("structure", {}) - return structure.get("unified", False) or structure.get("single", False) - - # Check for Unified structure (apps/api exists) - if os.path.isdir(os.path.join(self.current_dir, "apps", "api")): - return True - - # Check for Predefined structure (apps/ exists but not apps/api) - if os.path.isdir(os.path.join(self.current_dir, "apps")): - return False - - # Check for Single Folder structure (models/ and api/ exist in project dir) - project_dir, _ = CommonUtils.find_project_dir(self.current_dir) - if project_dir: - has_models = os.path.isdir(os.path.join(project_dir, "models")) - has_api = os.path.isdir(os.path.join(project_dir, "api")) - if has_models and has_api: - return True - - return False - - def _create_predefined_app(self, apps_dir: str) -> bool: - """Create an app following the predefined nested structure with custom templates.""" - app_dir = os.path.join(apps_dir, self.app_name) - os.makedirs(app_dir, exist_ok=True) - CommonUtils.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}" - app_config_name = self.app_name.title().replace("_", "") - base_context = { - "app_name": self.app_name, - "app_config_name": app_config_name, - "app_module": app_module, - "model_class_name": model_class_name, - } - - CommonUtils.create_file_from_template( - os.path.join(app_dir, "apps.py"), - "components/apps.py-tpl", - {"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.py-tpl")], - "serializers": [(f"{self.app_name}_serializer.py", "presets/predefined/apps/generic/serializers.py-tpl")], - "services": [(f"{self.app_name}_service.py", "presets/predefined/apps/generic/services.py-tpl")], - "views": [(f"{self.app_name}_view.py", "presets/predefined/apps/generic/views.py-tpl")], - "tests": [(f"test_{self.app_name}_api.py", "presets/predefined/apps/generic/tests.py-tpl")], - } - for folder, files in subfolders.items(): - folder_path = os.path.join(app_dir, folder) - CommonUtils.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) - CommonUtils.create_file_from_template( - file_path, template, base_context, f"Created apps/{self.app_name}/{folder}/{filename}" - ) - - CommonUtils.create_file_from_template( - os.path.join(app_dir, "urls.py"), - "config/urls.py-tpl", - { - "url_type": "drf_router", - "app_module": app_module, - "view_file": f"{self.app_name}_view", - "viewset_name": f"{model_class_name}ViewSet", - "router_prefix": "", - "basename": self.app_name, - "has_viewset": True, - "has_include": False, - }, - 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 diff --git a/djinit/services/files.py b/djinit/services/files.py deleted file mode 100644 index dc32929..0000000 --- a/djinit/services/files.py +++ /dev/null @@ -1,469 +0,0 @@ -""" -File management utilities for djinit. -Handles creation and modification of project files. -""" - -import os - -from djinit.core.base import BaseService -from djinit.services.templates import template_engine -from djinit.utils.common import CommonUtils - - -class FileManager(BaseService): - def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict): - super().__init__(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 CommonUtils.change_cwd(target_dir): - content = template_engine.render_template(template_path, context) - CommonUtils.create_file_with_content(filepath, content, message, should_format=should_format) - - def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None: - """Helper to create multiple folders and files from a specification dict.""" - base_context = base_context or {} - - for folder_name, file_templates in folder_specs.items(): - folder_path = os.path.join(base_dir, folder_name) - CommonUtils.create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py") - - for filename, template_path in file_templates: - file_path = os.path.join(folder_path, filename) - CommonUtils.create_file_from_template(file_path, template_path, base_context, f"Created {file_path}") - - def _create_apps_py(self, app_dir: str, app_name: str, message: str = None, should_format: bool = False) -> None: - """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" - CommonUtils.create_file_from_template( - apps_py_path, "components/apps.py-tpl", context, msg, should_format=should_format - ) - - def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None: - """Helper to create multiple subdirectories with __init__.py files.""" - for subdir in subdirs: - subdir_path = os.path.join(base_dir, subdir) - message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py" - CommonUtils.create_directory_with_init(subdir_path, message) - - def _create_settings_package(self, settings_dir: str, base_context: dict, prefix: str) -> None: - """Helper to create settings package files (base, development, production).""" - from djinit.utils.secretkey import generate_secret_key - - secret_key = generate_secret_key() - dev_context = {"secret_key": secret_key} - - # Prepare context for base.py with full AppConfig paths - base_settings_context = base_context.copy() - if "app_names" in base_settings_context: - transformed_apps = [] - for app in base_settings_context["app_names"]: - # Transform to full AppConfig path: module.path -> module.path.apps.ConfigName - short_name = app.split(".")[-1] - config_name = short_name.title().replace("_", "") + "Config" - # If it's the special "apps" container in unified structure, we might want to skip or handle differently - # But assumes all user apps follow standard djinit structure - full_path = f"{app}.apps.{config_name}" - transformed_apps.append(full_path) - base_settings_context["app_names"] = transformed_apps - - for filename, context in [ - ("base.py", base_settings_context), - ("development.py", dev_context), - ("production.py", base_settings_context), - ]: - filepath = os.path.join(settings_dir, filename) - template_path = f"config/settings/{filename.replace('.py', '')}.py-tpl" - 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 = {"url_type": "project", "api_module": api_module, "include_api": True} - if comment_out_api: - urls_context["comment_out_api_url"] = True - - files = [ - ("urls.py", "config/urls.py-tpl", urls_context), - ("wsgi.py", "config/wsgi.py-tpl", {"project_name": prefix}), - ("asgi.py", "config/asgi.py-tpl", {"project_name": prefix}), - ] - - for filename, template, context in files: - 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-tpl", {}, "Created .gitignore file") - - def create_requirements(self) -> None: - context = { - "use_database_url": self.metadata.get("use_database_url", True), - "database_type": self.metadata.get("database_type", "postgresql"), - } - self._render_and_create_file( - "requirements.txt", - "project/requirements-tpl", - 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.md-tpl", context, "Created README.md file") - - def create_env_file(self) -> None: - """Create .env.sample file with environment variables.""" - context = { - "project_name": self.module_name, - "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-tpl", - context, - "Created .env.sample file with generated secret key", - ) - - def create_pyproject(self, metadata: dict) -> None: - package_name = metadata.get("package_name", "backend") - package_name = CommonUtils.get_package_name(package_name) - context = { - "package_name": package_name, - "project_name": self.project_name, - "database_type": metadata.get("database_type", "postgresql"), - } - self._render_and_create_file( - "pyproject.toml", - "project/pyproject.toml-tpl", - context, - "Created pyproject.toml file for uv", - ) - - def create_project_urls(self) -> None: - """Create project urls.py using project_config/urls.py-tpl template (replaces update_project_urls).""" - effective_app_modules = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata) - context = { - "url_type": "project", - "project_name": self.project_name, - "app_names": effective_app_modules, - "include_apps": True, - "include_api": True, - } - self._render_and_create_file( - "urls.py", - "config/urls.py-tpl", - 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-tpl", - context, - "Created justfile with Django development tasks", - ) - - def create_predefined_structure(self) -> None: - apps_dir = os.path.join(self.project_root, "apps") - CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py") - - users_dir = os.path.join(apps_dir, "users") - CommonUtils.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.py-tpl")], - "serializers": [("user_serializer.py", "presets/predefined/apps/users/serializers.py-tpl")], - "services": [("user_service.py", "presets/predefined/apps/users/services.py-tpl")], - "views": [("user_view.py", "presets/predefined/apps/users/views.py-tpl")], - "tests": [("test_user_api.py", "presets/predefined/apps/users/tests.py-tpl")], - } - self._create_files_from_specs(users_dir, users_subfolders, {"app_module": "apps.users"}) - - users_urls_path = os.path.join(users_dir, "urls.py") - CommonUtils.create_file_from_template( - users_urls_path, - "config/urls.py-tpl", - { - "url_type": "drf_router", - "app_module": "apps.users", - "view_file": "user_view", - "viewset_name": "UserViewSet", - "router_prefix": "users", - "basename": "users", - "has_viewset": True, - "has_app_name": True, - "app_name": "users", - "has_include": True, - }, - "Created apps/users/urls.py", - ) - - core_dir = os.path.join(apps_dir, "core") - CommonUtils.create_directory_with_init(core_dir, "Created apps/core/__init__.py") - - core_subfolders = { - "utils": [("responses.py", "presets/predefined/core/utils/responses.py-tpl")], - "mixins": [("timestamped_model.py", "presets/predefined/core/mixins/timestamped_model.py-tpl")], - "middleware": [("request_logger.py", "presets/predefined/core/middleware/request_logger.py-tpl")], - } - self._create_files_from_specs(core_dir, core_subfolders, {}) - - exceptions_path = os.path.join(core_dir, "exceptions.py") - CommonUtils.create_file_from_template( - exceptions_path, "presets/predefined/core/exceptions.py-tpl", {}, "Created apps/core/exceptions.py" - ) - - api_dir = os.path.join(self.project_root, "api") - CommonUtils.create_directory_with_init(api_dir, "Created api/__init__.py") - api_urls_path = os.path.join(api_dir, "urls.py") - CommonUtils.create_file_from_template( - api_urls_path, - "config/urls.py-tpl", - {"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "api"}, - "Created api/urls.py", - ) - - api_v1_dir = os.path.join(api_dir, "v1") - CommonUtils.create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py") - api_v1_urls_path = os.path.join(api_v1_dir, "urls.py") - CommonUtils.create_file_from_template( - api_v1_urls_path, - "config/urls.py-tpl", - {"url_type": "api_version", "app_name": "v1", "app_list": ["users"], "app_module": "apps"}, - "Created api/v1/urls.py", - ) - - project_urls_path = os.path.join(self.project_configs, "urls.py") - CommonUtils.create_file_from_template( - project_urls_path, - "config/urls.py-tpl", - {"url_type": "project", "api_module": "api", "include_api": True}, - "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") - CommonUtils.create_directory_with_init(core_dir, "Created core/__init__.py") - - settings_dir = os.path.join(core_dir, "settings") - CommonUtils.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") - CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py") - - # Create apps.py for the 'apps' app - self._create_apps_py(apps_dir, "apps", "Created apps/apps.py", should_format=True) - - # 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") - CommonUtils.create_directory_with_init(api_dir, "Created apps/api/__init__.py") - - CommonUtils.create_file_from_template( - os.path.join(api_dir, "urls.py"), - "config/urls.py-tpl", - {"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "apps.api"}, - "Created apps/api/urls.py", - should_format=True, - ) - - api_v1_dir = os.path.join(api_dir, "v1") - CommonUtils.create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py") - - CommonUtils.create_file_from_template( - os.path.join(api_v1_dir, "urls.py"), - "config/urls.py-tpl", - {"url_type": "api_version", "app_name": "v1", "app_list": [], "app_module": "apps"}, - "Created apps/api/v1/urls.py", - should_format=True, - ) - - def create_single_structure(self) -> None: - project_dir = os.path.join(self.project_root, self.module_name) - CommonUtils.create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py") - - settings_dir = os.path.join(project_dir, "settings") - CommonUtils.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}/") - - CommonUtils.create_file_from_template( - os.path.join(project_dir, "admin", "__init__.py"), - "components/admin.py-tpl", - {}, - f"Created {self.module_name}/admin/__init__.py", - ) - - CommonUtils.create_file_from_template( - os.path.join(project_dir, "models", "__init__.py"), - "components/models.py-tpl", - {}, - f"Created {self.module_name}/models/__init__.py", - ) - - CommonUtils.create_file_from_template( - os.path.join(project_dir, "api", "README.md"), - "components/api_readme.md-tpl", - {"module_name": self.module_name}, - f"Created {self.module_name}/api/README.md", - ) - - CommonUtils.create_file_from_template( - os.path.join(project_dir, "models", "README.md"), - "components/models_readme.md-tpl", - {"module_name": self.module_name}, - f"Created {self.module_name}/models/README.md", - ) - - def create_procfile(self) -> None: - context = {"project_name": self.project_name} - self._render_and_create_file( - "Procfile", - "project/procfile-tpl", - 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-tpl", - 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-tpl", - 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-tpl", - 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") - CommonUtils.create_file_with_content( - filepath, json.dumps(config, indent=4), "Created .djinit configuration file" - ) diff --git a/djinit/services/project.py b/djinit/services/project.py deleted file mode 100644 index af17eb3..0000000 --- a/djinit/services/project.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -Project management for djinit. -Handles creation of Django projects and apps. -""" - -import os - -from djinit.core.base import BaseService -from djinit.ui.console import UIFormatter -from djinit.utils.common import CommonUtils -from djinit.utils.django import DjangoHelper - - -class ProjectManager(BaseService): - def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict): - super().__init__(project_root=os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)) - self.project_dir = project_dir - self.project_name = project_name - self.app_names = app_names - self.metadata = metadata - # self.project_root is set by BaseService via super().__init__ but we need to ensure it matches logic - # logic was: self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir) - # which I passed to super. So I can remove the line setting self.project_root here. - self.module_name = metadata.get("project_module_name") or self.project_name - - def create_project(self) -> None: - os.makedirs(self.project_root, exist_ok=True) - - unified = self.metadata.get("unified_structure", False) - DjangoHelper.startproject(self.module_name, self.project_root, unified=unified) - 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: - CommonUtils.create_directory_with_init( - apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py" - ) - - # Calculate module paths for correct AppConfig generation (e.g. src.users) - app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata) - - for i, app_name in enumerate(self.app_names): - app_module = app_module_paths[i] - DjangoHelper.startapp(app_name, apps_base_dir, app_module) - UIFormatter.print_success(f"Django app '{app_name}' created successfully!") - - self.add_apps_to_settings() - - def add_apps_to_settings(self) -> None: - """Add all apps to USER_DEFINED_APPS in base.py settings file.""" - base_settings_path = CommonUtils.get_base_settings_path(self.project_root, self.module_name) - - if not os.path.exists(base_settings_path): - from djinit.utils.exceptions import ConfigError - - raise ConfigError("Could not find base.py settings file") - - content = CommonUtils.read_base_settings(self.project_root, self.module_name) - if content is None: - from djinit.utils.exceptions import ConfigError - - raise ConfigError("Could not read base.py settings file") - - app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata) - - # Transform to full AppConfig paths (e.g. apps.users.apps.UsersConfig) - full_config_paths = [] - for app in app_module_paths: - short_name = app.split(".")[-1] - config_name = short_name.title().replace("_", "") + "Config" - full_config_paths.append(f"{app}.apps.{config_name}") - - existing_apps = CommonUtils.extract_existing_apps(content) - - # Check against both module path and full config path to avoid duplicates - apps_to_add = [] - for i, full_path in enumerate(full_config_paths): - mod_path = app_module_paths[i] - if full_path not in existing_apps and mod_path not in existing_apps: - apps_to_add.append(full_path) - elif mod_path in existing_apps: - # If legacy module path exists, we could upgrade it here, - # but ProjectManager is typically initialized on creation. - # For now, let's just not add a duplicate. - # Actually, let's upgrade it if we can - pass - - # If we found apps to add, insert them - if not apps_to_add: - # Check if we need to upgrade any legacy paths - upgraded = False - for i, full_path in enumerate(full_config_paths): - mod_path = app_module_paths[i] - if mod_path in existing_apps and full_path not in existing_apps: - content = CommonUtils.replace_app_in_user_defined_apps(content, mod_path, full_path) - upgraded = True - - if upgraded: - with open(base_settings_path, "w") as f: - f.write(content) - UIFormatter.print_success("Upgraded existing apps to full AppConfig paths") - return - - UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS") - return - - updated_content = CommonUtils.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}") diff --git a/djinit/services/templates.py b/djinit/services/templates.py deleted file mode 100644 index 69a3e9f..0000000 --- a/djinit/services/templates.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Template engine for djinit. -Handles loading and rendering Jinja2 templates for file generation. -""" - -import os -from pathlib import Path -from typing import Any, Dict - -from djinit.core.base import BaseService -from djinit.core.parser import InFileLogicParser - - -class TemplateEngine(BaseService): - """Template engine for rendering file templates.""" - - def __init__(self, template_dir: str = None): - """Initialize template engine. - - Args: - 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" - - super().__init__() - - if not template_dir.exists(): - raise FileNotFoundError(f"Template directory not found: {template_dir}") - - self.template_dir = str(template_dir) - self.parser = InFileLogicParser() - - def render_template(self, template_name: str, context: Dict[str, Any]) -> str: - """Render a template with the given context. - - Args: - template_name: Name of the template file (e.g., 'gitignore.py-tpl') - context: Dictionary of variables to pass to the template - - Returns: - Rendered template content as string - """ - template_path = os.path.join(self.template_dir, template_name) - if not os.path.exists(template_path): - raise FileNotFoundError(f"Template not found: {template_path}") - - with open(template_path) as f: - template_text = f.read() - - return self.parser.render(template_text, context) - - def render_string(self, template_string: str, context: Dict[str, Any]) -> str: - """Render a template string with the given context. - - Args: - template_string: Template content as string - context: Dictionary of variables to pass to the template - - Returns: - Rendered template content as string - """ - return self.parser.render(template_string, context) - - def get_template_names(self) -> list[str]: - """Get list of available template files.""" - if not os.path.exists(self.template_dir): - return [] - - templates = [] - for root, _, files in os.walk(self.template_dir): - for file in files: - if file.endswith("-tpl"): - rel_path = os.path.relpath(os.path.join(root, file), self.template_dir) - templates.append(rel_path) - return sorted(templates) - - def template_exists(self, template_name: str) -> bool: - """Check if a template file exists. - - Args: - template_name: Name of the template file - """ - template_path = os.path.join(self.template_dir, template_name) - return os.path.exists(template_path) - - -template_engine = TemplateEngine() diff --git a/djinit/ui/__init__.py b/djinit/ui/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/djinit/ui/console.py b/djinit/ui/console.py deleted file mode 100644 index a9f07c4..0000000 --- a/djinit/ui/console.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -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() diff --git a/djinit/ui/input.py b/djinit/ui/input.py deleted file mode 100644 index 7482a28..0000000 --- a/djinit/ui/input.py +++ /dev/null @@ -1,337 +0,0 @@ -""" -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 CommonUtils -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 = CommonUtils.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 = CommonUtils.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" diff --git a/djinit/utils/__init__.py b/djinit/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/djinit/utils/common.py b/djinit/utils/common.py deleted file mode 100644 index 3a1e153..0000000 --- a/djinit/utils/common.py +++ /dev/null @@ -1,340 +0,0 @@ -""" -Shared utility functions. -Contains common operations used across multiple managers. -""" - -import os -import subprocess -import sys -from contextlib import contextmanager - -from djinit.core.base import BaseUtils -from djinit.services.templates import template_engine -from djinit.ui.console import UIFormatter - - -class CommonUtils(BaseUtils): - @staticmethod - def format_file(filename: str) -> None: - """Format Python file using Ruff formatter.""" - subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True) - - @staticmethod - def create_file_with_content( - filename: str, content: str, success_message: str, should_format: bool = False - ) -> None: - from djinit.utils.exceptions import FileError - - try: - with open(filename, "w") as file: - file.write(content) - except OSError as e: - raise FileError(f"Failed to write file: {filename}", details=str(e)) from e - - if should_format: - CommonUtils.format_file(filename) - - UIFormatter.print_success(success_message) - - @staticmethod - def create_file_from_template( - file_path: str, - template_path: str, - context: dict, - success_message: str, - should_format: bool = True, - ) -> None: - try: - content = template_engine.render_template(template_path, context) - except Exception as e: - from djinit.utils.exceptions import TemplateError - - raise TemplateError(f"Failed to render template: {template_path}", details=str(e)) from e - - CommonUtils.create_file_with_content(file_path, content, success_message, should_format=should_format) - - @staticmethod - def create_init_file(directory: str, success_message: str) -> None: - init_path = os.path.join(directory, "__init__.py") - CommonUtils.create_file_from_template( - init_path, "project/init.py-tpl", {}, success_message, should_format=False - ) - - @staticmethod - def create_directory_with_init(dir_path: str, message: str) -> None: - """Create directory and __init__.py file.""" - os.makedirs(dir_path, exist_ok=True) - CommonUtils.create_init_file(dir_path, message) - - @staticmethod - def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None: - """Create multiple files from templates. - - Args: - base_dir: Base directory where files will be created - files: List of tuples (filename, template_path, context) - prefix: Optional prefix for success message - should_format: Whether to format the created files - """ - for file_spec in files: - if len(file_spec) == 3: - filename, template_path, context = file_spec - else: - # Support (filename, template_path) format with empty context - filename, template_path = file_spec - context = {} - filepath = os.path.join(base_dir, filename) - message = f"Created {prefix}{filename}" if prefix else f"Created {filename}" - CommonUtils.create_file_from_template( - filepath, template_path, context, message, should_format=should_format - ) - - @staticmethod - def get_package_name(project_dir: str) -> str: - """Get package name, defaulting to 'backend' if project_dir is '.' or empty.""" - return "backend" if project_dir == "." or not project_dir else project_dir - - @staticmethod - @contextmanager - def change_cwd(path: str): - original_cwd = os.getcwd() - os.chdir(path) - try: - yield - finally: - os.chdir(original_cwd) - - @staticmethod - def _parse_app_entry_from_line(line: str) -> str | None: - """Extract app name from a line in USER_DEFINED_APPS section.""" - line_stripped = line.strip().strip(",").strip() - for quote in ('"', "'"): - if line_stripped.startswith(quote) and line_stripped.endswith(quote): - return line_stripped.strip(quote) - return None - - @staticmethod - def _is_user_apps_section_start(line: str) -> bool: - """Check if line starts USER_DEFINED_APPS section.""" - return "USER_DEFINED_APPS" in line and "=" in line - - @staticmethod - def _iterate_user_apps_lines(content: str): - """Generator that yields lines within USER_DEFINED_APPS section.""" - lines = content.split("\n") - in_user_apps = False - - for line in lines: - if CommonUtils._is_user_apps_section_start(line): - in_user_apps = True - elif in_user_apps and line.strip() == "]": - break - elif in_user_apps: - yield line - - @staticmethod - def extract_existing_apps(content: str) -> set: - existing_apps = set() - for line in CommonUtils._iterate_user_apps_lines(content): - app_name = CommonUtils._parse_app_entry_from_line(line) - if app_name: - existing_apps.add(app_name) - return existing_apps - - @staticmethod - def format_app_entries(apps: list) -> list: - return [f' "{app}",' for app in apps] - - @staticmethod - def is_app_section_start(line: str) -> bool: - return "=" in line and any( - keyword in line for keyword in ("BUILT_IN_APPS", "THIRD_PARTY_APPS", "INSTALLED_APPS") - ) - - @staticmethod - def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool: - for next_idx in range(current_idx + 1, min(current_idx + 3, len(lines))): - next_line = lines[next_idx].strip() - if next_line and "BUILT_IN_APPS" in next_line: - return True - return True # Default to True if we're inside USER_DEFINED_APPS - - @staticmethod - def split_bracket_line(line: str, apps_to_add: list) -> list: - bracket_pos = line.rfind("]") - before_bracket = line[:bracket_pos].rstrip() if bracket_pos > 0 else line.rstrip() - after_bracket = line[bracket_pos:] if bracket_pos > 0 else "]" - - result = [before_bracket] - result.extend(CommonUtils.format_app_entries(apps_to_add)) - result.append(after_bracket) - return result - - @staticmethod - def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str: - lines = content.split("\n") - new_lines = [] - in_user_apps = False - apps_added = False - - for i, line in enumerate(lines): - if CommonUtils._is_user_apps_section_start(line) and not apps_added: - in_user_apps = True - - if "]" in line: - new_lines.extend(CommonUtils.split_bracket_line(line, apps_to_add)) - apps_added = True - in_user_apps = False - else: - new_lines.append(line) - - elif in_user_apps and line.strip() == "]" and not apps_added: - if CommonUtils.is_user_apps_closing_bracket(lines, i): - new_lines.extend(CommonUtils.format_app_entries(apps_to_add)) - new_lines.append(line) - apps_added = True - in_user_apps = False - else: - new_lines.append(line) - - elif in_user_apps and CommonUtils.is_app_section_start(line) and not apps_added: - new_lines.extend(CommonUtils.format_app_entries(apps_to_add)) - new_lines.append("]") - new_lines.append(line) - apps_added = True - in_user_apps = False - - else: - new_lines.append(line) - - if not apps_added: - UIFormatter.print_error("Could not find or update USER_DEFINED_APPS section in base.py") - return None - - return "\n".join(new_lines) - - @staticmethod - def replace_app_in_user_defined_apps(content: str, old_app: str, new_app: str) -> str | None: - """Replace an app entry in USER_DEFINED_APPS with a new one.""" - lines = content.split("\n") - new_lines = [] - replaced = False - - for line in lines: - if not replaced and CommonUtils._parse_app_entry_from_line(line) == old_app: - # Try to preserve indentation - indent_idx = 0 - for char in line: - if char.isspace(): - indent_idx += 1 - else: - break - indent = line[:indent_idx] if indent_idx > 0 else " " - - new_lines.append(f'{indent}"{new_app}",') - replaced = True - else: - new_lines.append(line) - - if replaced: - return "\n".join(new_lines) - return None - - @staticmethod - def calculate_app_module_paths(app_names: list, metadata: dict) -> list: - nested = bool(metadata.get("nested_apps")) - nested_dir_name = metadata.get("nested_dir") if nested else None - return [CommonUtils.calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names] - - @staticmethod - def calculate_app_module_path(app_name: str, nested: bool, nested_dir: str | None) -> str: - return f"{nested_dir}.{app_name}" if nested and nested_dir else app_name - - @staticmethod - def is_django_project(directory: str = None) -> bool: - if directory is None: - directory = os.getcwd() - manage_py_path = os.path.join(directory, "manage.py") - return os.path.exists(manage_py_path) - - @staticmethod - def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]: - if search_dir is None: - search_dir = os.getcwd() - - for item in os.listdir(search_dir): - if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__": - candidate = os.path.join(search_dir, item) - base_py = os.path.join(candidate, "settings", "base.py") - if os.path.exists(base_py): - return candidate, base_py - return None, None - - @staticmethod - def find_settings_path(search_dir: str = None) -> str | None: - project_dir, settings_base_path = CommonUtils.find_project_dir(search_dir) - if settings_base_path: - return os.path.dirname(settings_base_path) - return None - - @staticmethod - def get_base_settings_path(project_root: str, project_name: str) -> str: - settings_dir = os.path.join(project_root, project_name, "settings") - return os.path.join(settings_dir, "base.py") - - @staticmethod - def read_base_settings(project_root: str, project_name: str) -> str | None: - base_settings_path = CommonUtils.get_base_settings_path(project_root, project_name) - if not os.path.exists(base_settings_path): - return None - - with open(base_settings_path) as f: - return f.read() - - @staticmethod - def detect_nested_structure_from_settings( - base_settings_path: str, search_dir: str = None - ) -> tuple[bool, str | None, str]: - if search_dir is None: - search_dir = os.getcwd() - - if not os.path.exists(base_settings_path): - return False, None, search_dir - - try: - with open(base_settings_path) as f: - base_content = f.read() - - for line in CommonUtils._iterate_user_apps_lines(base_content): - app_label = CommonUtils._parse_app_entry_from_line(line) - if app_label and "." in app_label: - nested_dir_name = app_label.split(".", 1)[0] - nested_path = os.path.join(search_dir, nested_dir_name) - if os.path.isdir(nested_path): - return True, nested_dir_name, nested_path - except Exception: - pass - - return False, None, search_dir - - @staticmethod - def get_djinit_config(project_root: str = None) -> dict | None: - """Read .djinit configuration file.""" - import json - - if project_root is None: - project_root = os.getcwd() - - config_path = os.path.join(project_root, ".djinit") - if not os.path.exists(config_path): - # Try finding it in parent directories - parent = os.path.dirname(project_root) - if parent and parent != project_root: - return CommonUtils.get_djinit_config(parent) - return None - - try: - with open(config_path) as f: - return json.load(f) - except Exception: - return None diff --git a/djinit/utils/django.py b/djinit/utils/django.py deleted file mode 100644 index 7eeabba..0000000 --- a/djinit/utils/django.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -Django helper module that replicates Django's startproject and startapp functionality. - -This module provides functions to create Django projects and apps without requiring Django. -""" - -import os - -from djinit.core.config import DJANGO_VERSION -from djinit.utils.common import CommonUtils - - -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") - CommonUtils.create_file_from_template( - manage_py_path, "project/manage.py-tpl", {"project_name": project_name}, "Created manage.py" - ) - os.chmod(manage_py_path, 0o755) - - if unified: - return - - project_config_dir = os.path.join(directory, project_name) - CommonUtils.create_directory_with_init(project_config_dir, f"Created {project_name}/__init__.py") - - settings_dir = os.path.join(project_config_dir, "settings") - CommonUtils.create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py") - - base_context = {"project_name": project_name, "app_names": []} - settings_files = [ - ("base.py", "config/settings/base.py-tpl", base_context), - ("development.py", "config/settings/development.py-tpl", {}), - ("production.py", "config/settings/production.py-tpl", {}), - ] - CommonUtils.create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/") - - urls_context = { - "url_type": "project", - "project_name": project_name, - "django_version": DjangoHelper.DJANGO_VERSION, - } - project_files = [ - ("urls.py", "config/urls.py-tpl", urls_context), - ("wsgi.py", "config/wsgi.py-tpl", {}), - ("asgi.py", "config/asgi.py-tpl", {}), - ] - CommonUtils.create_files_from_templates(project_config_dir, project_files, f"{project_name}/") - - except Exception as e: - from djinit.utils.exceptions import DjinitError - - raise DjinitError(f"Error creating Django project: {e}", details=str(e)) from e - - @staticmethod - def startapp(app_name: str, directory: str, app_module: str = None) -> None: - try: - app_dir = os.path.join(directory, app_name) - os.makedirs(app_dir, exist_ok=True) - - # Prepare context - app_config_name = app_name.title().replace("_", "") - context = { - "app_name": app_name, - "app_config_name": app_config_name, - "django_version": DjangoHelper.DJANGO_VERSION, - } - - # Use provided app_module or default to app_name (path calculation handled by caller) - if not app_module: - app_module = app_name - - context["app_module"] = app_module - - CommonUtils.create_init_file(app_dir, f"Created {app_name}/__init__.py") - - app_files = [ - ("apps.py", "components/apps.py-tpl", context), - ("models.py", "components/models.py-tpl", context), - ("views.py", "components/views.py-tpl", context), - ("admin.py", "components/admin.py-tpl", context), - ("urls.py", "config/urls.py-tpl", {"url_type": "component"}), - ("serializers.py", "components/serializers.py-tpl", context), - ("routes.py", "components/routes.py-tpl", context), - ("tests.py", "components/tests.py-tpl", context), - ] - - CommonUtils.create_files_from_templates(app_dir, app_files, f"{app_name}/") - - migrations_dir = os.path.join(app_dir, "migrations") - CommonUtils.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 diff --git a/djinit/utils/exceptions.py b/djinit/utils/exceptions.py deleted file mode 100644 index f8e8b65..0000000 --- a/djinit/utils/exceptions.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -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 diff --git a/djinit/utils/secretkey.py b/djinit/utils/secretkey.py deleted file mode 100644 index 67daba5..0000000 --- a/djinit/utils/secretkey.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Django Secret Key Generator - -Generate secure Django secret keys for different environments. -""" - -import secrets -import string - -from rich.console import Console -from rich.table import Table - -console = Console() - - -def generate_secret_key(length: int = 50) -> str: - alphabet = string.ascii_letters + string.digits + "!@#$%^&*(-_=+)" - return "".join(secrets.choice(alphabet) for _ in range(length)) - - -def generate_multiple_keys(count: int = 3, length: int = 50) -> list[str]: - return [generate_secret_key(length) for _ in range(count)] - - -def display_secret_keys(keys: list[str]) -> None: - table = Table(title="🔐 Django Secret Keys", show_header=True, header_style="bold blue") - table.add_column("Environment", style="cyan", no_wrap=True) - table.add_column("Secret Key", style="dim") - - for i, key in enumerate(keys, 1): - table.add_row(f"Environment {i}", key) - - console.print(table) - console.print() - - -if __name__ == "__main__": - keys = generate_multiple_keys(3, 50) - display_secret_keys(keys) diff --git a/djinit/utils/validators.py b/djinit/utils/validators.py deleted file mode 100644 index 4562a0a..0000000 --- a/djinit/utils/validators.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Input validation utilities for Django project setup. -Handles validation of project names, app names, and other user inputs. -""" - -import keyword -import re -from typing import Any, Callable, Tuple - -from djinit.core.config import PYTHON_BUILTINS - -NAME_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*$") - - -def _validate_name(name: str, name_type: str = "name") -> Tuple[bool, str]: - validation_rules: list[Tuple[Callable[[str], Any], str]] = [ - (lambda n: bool(n and n.strip()), f"{name_type.capitalize()} cannot be empty"), - (lambda n: len(n.strip()) >= 2, f"{name_type.capitalize()} must be at least 2 characters long"), - (lambda n: len(n.strip()) <= 50, f"{name_type.capitalize()} must be less than 50 characters"), - ( - lambda n: NAME_PATTERN.match(n.strip()), - f"{name_type.capitalize()} must start with a letter and contain only letters, numbers, and underscores", - ), - ( - lambda n: not keyword.iskeyword(n.strip()), - f"'{name.strip()}' is a Python keyword. Please choose a different name", - ), - ( - lambda n: n.strip().lower() not in PYTHON_BUILTINS, - f"'{name.strip()}' conflicts with Python builtin module. Choose a different name", - ), - (lambda n: not n.strip().startswith("_"), f"{name_type.capitalize()} should not start with underscore"), - ] - - for rule, error_message in validation_rules: - if not rule(name): - return False, error_message - - return True, "" - - -def validate_project_name(name: str) -> Tuple[bool, str]: - return _validate_name(name, "project name") - - -def validate_app_name(name: str) -> Tuple[bool, str]: - return _validate_name(name, "app name") -- 2.43.0 From 7d76fb902ca752413075de59acaf9887072da3cf Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 22:23:51 +0545 Subject: [PATCH 19/24] feat(src/djinit): add CLI and core utility modules Introduce command-line interface and supporting modules for project initialization. - New Features: - Add `cli.py` with Typer commands `secret` and `app` - Implement custom exception hierarchy in `utils/exceptions.py` - Provide secret key generation utilities in `utils/secretkey.py` - Add name validation logic in `utils/validators.py` - Initialize package with empty `__init__.py` - Enhancements: - Update `pyproject.toml` to include new modules and console script entry points - Bug Fixes: - (none) --- src/djinit/__init__.py | 0 src/djinit/cli.py | 156 +++++++ src/djinit/core/__init__.py | 0 src/djinit/core/base.py | 52 +++ src/djinit/core/config.py | 14 + src/djinit/core/parser.py | 215 +++++++++ src/djinit/core/types.py | 48 ++ src/djinit/creators/__init__.py | 0 src/djinit/creators/app.py | 313 +++++++++++++ src/djinit/creators/files.py | 428 ++++++++++++++++++ src/djinit/creators/project.py | 201 ++++++++ src/djinit/creators/setup.py | 120 +++++ src/djinit/templater/__init__.py | 89 ++++ src/djinit/templater/base.py | 75 +++ src/djinit/templates/components/admin.py-tpl | 5 + .../templates/components/api_readme.md-tpl | 76 ++++ src/djinit/templates/components/apps.py-tpl | 12 + src/djinit/templates/components/models.py-tpl | 5 + .../templates/components/models_readme.md-tpl | 119 +++++ .../templates/components/serializers.py-tpl | 5 + src/djinit/templates/components/tests.py-tpl | 5 + src/djinit/templates/components/urls.py-tpl | 7 + src/djinit/templates/components/views.py-tpl | 5 + src/djinit/templates/config/asgi.py-tpl | 11 + .../templates/config/settings/base.py-tpl | 180 ++++++++ .../config/settings/development.py-tpl | 42 ++ .../config/settings/production.py-tpl | 65 +++ src/djinit/templates/config/urls.py-tpl | 103 +++++ src/djinit/templates/config/wsgi.py-tpl | 12 + .../templates/project/ci/github_actions-tpl | 109 +++++ src/djinit/templates/project/ci/gitlab_ci-tpl | 133 ++++++ src/djinit/templates/project/env_sample-tpl | 32 ++ src/djinit/templates/project/gitignore-tpl | 93 ++++ src/djinit/templates/project/init.py-tpl | 0 src/djinit/templates/project/justfile-tpl | 219 +++++++++ src/djinit/templates/project/manage.py-tpl | 32 ++ src/djinit/templates/project/procfile-tpl | 20 + .../templates/project/pyproject.toml-tpl | 46 ++ src/djinit/templates/project/readme.md-tpl | 201 ++++++++ src/djinit/templates/project/requirements-tpl | 19 + src/djinit/templates/project/runtime_txt-tpl | 1 + src/djinit/ui/__init__.py | 0 src/djinit/ui/console.py | 206 +++++++++ src/djinit/ui/input.py | 337 ++++++++++++++ src/djinit/utils/__init__.py | 0 src/djinit/utils/common.py | 340 ++++++++++++++ src/djinit/utils/django.py | 116 +++++ src/djinit/utils/exceptions.py | 30 ++ src/djinit/utils/secretkey.py | 39 ++ src/djinit/utils/validators.py | 47 ++ 50 files changed, 4383 insertions(+) create mode 100644 src/djinit/__init__.py create mode 100644 src/djinit/cli.py create mode 100644 src/djinit/core/__init__.py create mode 100644 src/djinit/core/base.py create mode 100644 src/djinit/core/config.py create mode 100644 src/djinit/core/parser.py create mode 100644 src/djinit/core/types.py create mode 100644 src/djinit/creators/__init__.py create mode 100644 src/djinit/creators/app.py create mode 100644 src/djinit/creators/files.py create mode 100644 src/djinit/creators/project.py create mode 100644 src/djinit/creators/setup.py create mode 100644 src/djinit/templater/__init__.py create mode 100644 src/djinit/templater/base.py create mode 100644 src/djinit/templates/components/admin.py-tpl create mode 100644 src/djinit/templates/components/api_readme.md-tpl create mode 100644 src/djinit/templates/components/apps.py-tpl create mode 100644 src/djinit/templates/components/models.py-tpl create mode 100644 src/djinit/templates/components/models_readme.md-tpl create mode 100644 src/djinit/templates/components/serializers.py-tpl create mode 100644 src/djinit/templates/components/tests.py-tpl create mode 100644 src/djinit/templates/components/urls.py-tpl create mode 100644 src/djinit/templates/components/views.py-tpl create mode 100644 src/djinit/templates/config/asgi.py-tpl create mode 100644 src/djinit/templates/config/settings/base.py-tpl create mode 100644 src/djinit/templates/config/settings/development.py-tpl create mode 100644 src/djinit/templates/config/settings/production.py-tpl create mode 100644 src/djinit/templates/config/urls.py-tpl create mode 100644 src/djinit/templates/config/wsgi.py-tpl create mode 100644 src/djinit/templates/project/ci/github_actions-tpl create mode 100644 src/djinit/templates/project/ci/gitlab_ci-tpl create mode 100644 src/djinit/templates/project/env_sample-tpl create mode 100644 src/djinit/templates/project/gitignore-tpl create mode 100644 src/djinit/templates/project/init.py-tpl create mode 100644 src/djinit/templates/project/justfile-tpl create mode 100644 src/djinit/templates/project/manage.py-tpl create mode 100644 src/djinit/templates/project/procfile-tpl create mode 100644 src/djinit/templates/project/pyproject.toml-tpl create mode 100644 src/djinit/templates/project/readme.md-tpl create mode 100644 src/djinit/templates/project/requirements-tpl create mode 100644 src/djinit/templates/project/runtime_txt-tpl create mode 100644 src/djinit/ui/__init__.py create mode 100644 src/djinit/ui/console.py create mode 100644 src/djinit/ui/input.py create mode 100644 src/djinit/utils/__init__.py create mode 100644 src/djinit/utils/common.py create mode 100644 src/djinit/utils/django.py create mode 100644 src/djinit/utils/exceptions.py create mode 100644 src/djinit/utils/secretkey.py create mode 100644 src/djinit/utils/validators.py diff --git a/src/djinit/__init__.py b/src/djinit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/djinit/cli.py b/src/djinit/cli.py new file mode 100644 index 0000000..5b1cf51 --- /dev/null +++ b/src/djinit/cli.py @@ -0,0 +1,156 @@ +import logging +from typing import Annotated, Optional + +import typer +from rich.panel import Panel +from rich.text import Text + +from djinit.creators.app import AppCreator +from djinit.creators.setup import SetupCreator +from djinit.ui.console import UIColors, UIFormatter, console +from djinit.ui.input import get_user_input +from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys +from djinit.utils.validators import validate_app_name + +# Configure logger +logger = logging.getLogger(__name__) + +app = typer.Typer( + help="djinit - A CLI tool to set up Django projects with best practices.", + add_completion=False, + no_args_is_help=True, +) + + +def version_callback(value: bool): + if value: + from importlib.metadata import version + + try: + typer.echo(f"djinit v{version('djinitx')}") + except Exception: + typer.echo("djinit (version unknown)") + raise typer.Exit() + + +@app.callback() +def main( + version: Annotated[ + Optional[bool], + typer.Option("--version", "-v", callback=version_callback, is_eager=True, help="Show the version and exit."), + ] = None, +): + """ + djinit: Django Project Initializer + """ + pass + + +@app.command("secret") +def secret( + count: Annotated[int, typer.Option("--count", "-c", help="Number of keys to generate")] = 3, + length: Annotated[int, typer.Option("--length", "-l", help="Length of each secret key")] = 50, +): + """ + Generate Django secret keys. + """ + keys = generate_multiple_keys(count, length) + UIFormatter.print_info("") + display_secret_keys(keys) + + instructions = Text() + instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT) + instructions.append("1. Copy the appropriate secret key for your environment\n") + instructions.append("2. Add it to your .env file:\n") + instructions.append(" SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE) + instructions.append("3. Or set it as an environment variable:\n") + instructions.append(" export SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE) + instructions.append("4. Never commit secret keys to version control!\n", style=UIColors.WARNING) + + console.print(Panel(instructions, title="💡 How to Use", border_style="blue")) + console.print() + + +@app.command("app") +def create_app(app_names: Annotated[list[str], typer.Argument(help="Name(s) of the Django app(s) to create")]): + """ + Create one or more Django apps. + """ + UIFormatter.print_info("") + UIFormatter.print_header("Django App Creation") + UIFormatter.print_info("") + + # Process app names (handle commma separation if passed as single arg) + processed_names = [] + for name in app_names: + parts = [p.strip() for p in name.split(",") if p.strip()] + processed_names.extend(parts) + + processed_names = list(dict.fromkeys(processed_names)) # Dedup + + if not processed_names: + UIFormatter.print_error("App name cannot be empty") + raise typer.Exit(1) + + for name in processed_names: + is_valid, error_msg = validate_app_name(name) + if not is_valid: + UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}") + raise typer.Exit(1) + + any_failure = False + for name in processed_names: + creator = AppCreator(name) + success = creator.create_app() + if not success: + UIFormatter.print_error(f"Failed to create Django app '{name}'") + any_failure = True + break # Stop on failure? Or continue? Original code broke loop on failure. + + if any_failure: + raise typer.Exit(1) + + instructions = Text() + instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT) + instructions.append("1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n") + instructions.append("2. Create your models in each app's models.py\n") + instructions.append("3. Run migrations: just makemigrations\n") + instructions.append("4. Apply migrations: just migrate\n") + instructions.append("5. Create views, serializers and routes(URLs) for your app(s)\n") + + console.print(Panel(instructions, title="💡 What's Next", border_style="green")) + UIFormatter.print_info("") + + +@app.command("setup") +def setup(): + """ + Interactively set up a new Django project. + """ + UIFormatter.print_header("Django Project Setup") + + project_dir, project_name, primary_app, app_names, metadata = get_user_input() + + orchestrator = SetupCreator(project_dir, project_name, primary_app, app_names, metadata) + success = orchestrator.create() + + if success: + UIFormatter.print_info("") + UIFormatter.print_header("Setup Complete!") + console.print(f"[bold green]Project '{project_name}' has been successfully created![/bold green]") + + instructions = Text() + instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT) + instructions.append(f"1. cd {project_dir}\n") + instructions.append("2. Install dependencies: pip install -r requirements.txt\n") + instructions.append("3. Run migrations: just migrate (or python manage.py migrate)\n") + instructions.append("4. Start server: just run (or python manage.py runserver)\n") + + console.print(Panel(instructions, title="Get Started", border_style="green")) + else: + UIFormatter.print_error("Project setup failed. Please check the logs above.") + raise typer.Exit(1) + + +if __name__ == "__main__": + app() diff --git a/src/djinit/core/__init__.py b/src/djinit/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/djinit/core/base.py b/src/djinit/core/base.py new file mode 100644 index 0000000..452528a --- /dev/null +++ b/src/djinit/core/base.py @@ -0,0 +1,52 @@ +""" +Base classes for the djinit architecture. +Defines the common interface for services, commands, and utilities. +""" + +from abc import abstractmethod +from typing import Any + + +class BaseObject: + """ + Base object for all djinit classes. + """ + + def __init__(self, *args, **kwargs): + pass + + +class BaseService(BaseObject): + """ + Base class for all services. + Services handle business logic and orchestration. + """ + + def __init__(self, project_root: str = None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.project_root = project_root + + +class BaseCommand(BaseObject): + """ + Base class for all CLI commands. + Commands handle user input and invoke services. + """ + + def __init__(self, args: Any, *args_list, **kwargs): + super().__init__(*args_list, **kwargs) + self.args = args + + @abstractmethod + def execute(self) -> None: + """Execute the command.""" + pass + + +class BaseUtils(BaseObject): + """ + Base class for utility collections. + Utilities provide helper functions. + """ + + pass diff --git a/src/djinit/core/config.py b/src/djinit/core/config.py new file mode 100644 index 0000000..61b610e --- /dev/null +++ b/src/djinit/core/config.py @@ -0,0 +1,14 @@ +""" +Core configuration and constants for djinit. +""" + +import sys + +# Django version used for project generation +DJANGO_VERSION = "6.0.1" + +# 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) diff --git a/src/djinit/core/parser.py b/src/djinit/core/parser.py new file mode 100644 index 0000000..f672e23 --- /dev/null +++ b/src/djinit/core/parser.py @@ -0,0 +1,215 @@ +import re +from typing import Any, Dict, List + + +class InFileLogicParser: + """ + A lightweight template parser that uses comment-style markers for logic + and [[ variable ]] for substitutions. + """ + + def __init__(self, context: Dict[str, Any] = None): + self.context = context or {} + + def _get_value(self, key: str) -> str: + """ + Supports nested/dotted access like [[ user.name ]] or [[ settings["DEBUG"] ]]. + For simplicity, it uses eval in the provided context for complex expressions, + but falls back to safe dictionary lookup for simple keys. + """ + key = key.strip() + try: + # Try evaluating the expression in the context + return str(eval(key, {"__builtins__": {}}, self.context)) + except (NameError, SyntaxError, KeyError, TypeError, AttributeError): + # Fallback for common patterns or just return the key as is if it fails + return f"[[ {key} ]]" + + def render(self, template_text: str, context: Dict[str, Any] = None) -> str: + if context is not None: + self.context = context + + final_lines = [] + # Stack stores boolean results of nested IF blocks + # Each element is (current_block_result, has_any_true_branch_executed) + stack: List[List[bool]] = [] + + lines = template_text.splitlines() + i = 0 + while i < len(lines): + line = lines[i] + stripped = line.strip() + + # Handle @IF + if stripped.startswith("# @IF "): + expr = stripped[6:].strip() + try: + result = bool(eval(expr, {"__builtins__": {}}, self.context)) + except Exception: + result = False + stack.append([result, result]) + i += 1 + continue + + # Handle @ELSEIF + elif stripped.startswith("# @ELSEIF "): + if not stack: + final_lines.append(line) + i += 1 + continue + + expr = stripped[9:].strip() + current_stack = stack[-1] + + if current_stack[1]: + current_stack[0] = False + else: + try: + result = bool(eval(expr, {"__builtins__": {}}, self.context)) + except Exception: + result = False + current_stack[0] = result + if result: + current_stack[1] = True + i += 1 + continue + + # Handle @ELSE + elif stripped.startswith("# @ELSE"): + if not stack: + final_lines.append(line) + i += 1 + continue + + current_stack = stack[-1] + if current_stack[1]: + current_stack[0] = False + else: + current_stack[0] = True + current_stack[1] = True + i += 1 + continue + + # Handle @ENDIF + elif stripped.startswith("# @ENDIF"): + if stack: + stack.pop() + else: + final_lines.append(line) + i += 1 + continue + + # Handle @LOOP + elif stripped.startswith("# @LOOP "): + if stack and not all(s[0] for s in stack): + # Skip the entire loop block if inside a false conditional + loop_depth = 1 + i += 1 + while i < len(lines) and loop_depth > 0: + if lines[i].strip().startswith("# @LOOP "): + loop_depth += 1 + elif lines[i].strip().startswith("# @ENDLOOP"): + loop_depth -= 1 + i += 1 + continue + + parts = stripped[7:].strip().split(" in ") + if len(parts) == 2: + var_name = parts[0].strip() + iterable_name = parts[1].strip() + try: + iterable = eval(iterable_name, {"__builtins__": {}}, self.context) + except Exception: + iterable = [] + + # Capture loop body + loop_body = [] + i += 1 + loop_depth = 1 + while i < len(lines) and loop_depth > 0: + if lines[i].strip().startswith("# @LOOP "): + loop_depth += 1 + elif lines[i].strip().startswith("# @ENDLOOP"): + loop_depth -= 1 + + if loop_depth > 0: + loop_body.append(lines[i]) + i += 1 + + if i < len(lines): # Skip the @ENDLOOP line itself + i += 1 + + # Execute loop + if hasattr(iterable, "__iter__"): + old_val = self.context.get(var_name) + for val in iterable: + self.context[var_name] = val + rendered_body = self.render("\n".join(loop_body), self.context) + final_lines.extend(rendered_body.splitlines()) + if old_val is not None: + self.context[var_name] = old_val + else: + self.context.pop(var_name, None) + continue + else: + final_lines.append(line) + i += 1 + continue + + # Handle @ENDLOOP (should only be hit if out of sync or error) + elif stripped.startswith("# @ENDLOOP"): + i += 1 + continue + + # Check if we should skip this line based on conditional stack + if stack and not all(s[0] for s in stack): + i += 1 + continue + + # Variable substitution + rendered_line = line + matches = re.findall(r"\[\[\s*(.*?)\s*\]\]", rendered_line) + for match in matches: + value = self._get_value(match) + # Replace all variations of the variable syntax + rendered_line = re.sub(r"\[\[\s*" + re.escape(match) + r"\s*\]\]", value, rendered_line) + + # In-line @IF support + # Case 1: content # @IF cond + # Case 2: # @IF cond content + if "# @IF " in rendered_line: + parts = rendered_line.split("# @IF ", 1) + pre_content = parts[0].rstrip() + rest = parts[1].strip() + + # Split rest into expression and post-content (if any) + # This is tricky because the expression might have spaces. + # If there's an # @ENDIF on the same line, use it. + post_content = "" + expr = rest + if " # @ENDIF" in rest: + expr_parts = rest.split(" # @ENDIF", 1) + expr = expr_parts[0].strip() + post_content = expr_parts[1].lstrip() + elif "# @ENDIF" in rest: + expr_parts = rest.split("# @ENDIF", 1) + expr = expr_parts[0].strip() + post_content = expr_parts[1].lstrip() + + try: + if bool(eval(expr, {"__builtins__": {}}, self.context)): + # If Case 1, we want pre_content. If Case 2, we want post_content. + # Usually, if pre_content is empty (or just whitespace), it's Case 2. + if pre_content: + final_lines.append(pre_content + post_content) + else: + final_lines.append(post_content) + except Exception: + pass + i += 1 + continue + + final_lines.append(rendered_line) + i += 1 + + return "\n".join(final_lines) diff --git a/src/djinit/core/types.py b/src/djinit/core/types.py new file mode 100644 index 0000000..a70454c --- /dev/null +++ b/src/djinit/core/types.py @@ -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()) diff --git a/src/djinit/creators/__init__.py b/src/djinit/creators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/djinit/creators/app.py b/src/djinit/creators/app.py new file mode 100644 index 0000000..2281536 --- /dev/null +++ b/src/djinit/creators/app.py @@ -0,0 +1,313 @@ +""" +App management for djinit. +Handles creation of Django apps and updating settings. +""" + +import os +from typing import Optional + +from djinit.core.base import BaseService +from djinit.ui.console import UIFormatter +from djinit.utils.common import CommonUtils +from djinit.utils.django import DjangoHelper + + +class AppCreator(BaseService): + def __init__(self, app_name: str): + super().__init__() + self.app_name = app_name + self.current_dir = os.getcwd() + self.manage_py_path = os.path.join(self.current_dir, "manage.py") + self.config = CommonUtils.get_djinit_config(self.current_dir) + self._project_structure_cache = None + + def create_app(self) -> bool: + if not self._is_django_project(): + UIFormatter.print_error( + "Not in a Django project directory. Please run this command from your Django project root." + ) + return False + + # Check structure type + structure_type = self._get_structure_type() + + if structure_type in ["unified", "single"]: + UIFormatter.print_warning( + f"Creating apps in {structure_type} structure. " + "Note: You'll need to manually organize your code within the existing structure." + ) + + if self._app_exists(): + UIFormatter.print_error(f"Django app '{self.app_name}' already exists.") + return False + + if not self._create_django_app(): + return False + + if not self._add_to_installed_apps(): + return False + + UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!") + return True + + def _is_django_project(self) -> bool: + return CommonUtils.is_django_project(self.current_dir) + + def _get_structure_type(self) -> str: + """Determine the project structure type.""" + if self.config: + structure = self.config.get("structure", {}) + if structure.get("unified"): + return "unified" + elif structure.get("single"): + return "single" + elif structure.get("predefined"): + return "predefined" + else: + return "standard" + + # Fallback: detect from filesystem + if self._is_predefined_structure(): + return "predefined" + if self._is_restricted_structure(): + # Try to distinguish between unified and single + if os.path.isdir(os.path.join(self.current_dir, "apps", "api")): + return "unified" + return "single" + return "standard" + + def _calculate_app_module_path(self, structure_type: str) -> str: + """Calculate the module path for the app based on structure type.""" + if structure_type == "unified": + # Unified structure: apps are not separate, return app name within apps module + # But actually in unified, we don't create separate apps - the structure is different + # For now, return a path that makes sense: apps.{appname} + return f"apps.{self.app_name}" + elif structure_type == "single": + # Single structure: app is within the single module + module_name = self.config.get("project_name") if self.config else self.app_name + return f"{module_name}.{self.app_name}" + elif structure_type == "predefined": + # Predefined: apps are in apps/ directory + return f"apps.{self.app_name}" + else: + # Standard: may be nested or not + is_nested, nested_dir, _ = self._get_project_structure() + return CommonUtils.calculate_app_module_path(self.app_name, is_nested, nested_dir) + + def _app_exists(self) -> bool: + # For predefined structure, check in apps/ directory + if self._is_predefined_structure(): + app_path = os.path.join(self.current_dir, "apps", self.app_name) + else: + _, _, apps_base_dir = self._get_project_structure() + app_path = os.path.join(apps_base_dir, self.app_name) + return os.path.exists(app_path) + + def _create_django_app(self) -> bool: + if not os.path.exists(self.manage_py_path): + UIFormatter.print_error("Could not find manage.py file in project root") + return False + + if self._is_predefined_structure(): + return self._create_predefined_app(os.path.join(self.current_dir, "apps")) + + is_nested, nested_dir, apps_base_dir = self._get_project_structure() + + # Calculate app_module based on project structure + if is_nested and nested_dir: + # Normalize nested_dir (replace / with . for module path) + module_base = nested_dir.replace(os.path.sep, ".") + app_module = f"{module_base}.{self.app_name}" + else: + app_module = self.app_name + + success = DjangoHelper.startapp(self.app_name, apps_base_dir, app_module) + if success: + UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}") + else: + UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'") + return success + + def _add_to_installed_apps(self) -> bool: + structure_type = self._get_structure_type() + + # Find settings directory based on structure type + if structure_type == "unified": + settings_path = os.path.join(self.current_dir, "core", "settings") + elif structure_type == "single": + # For single structure, find the module name from config or detect it + module_name = self.config.get("project_name") if self.config else None + if not module_name: + # Fallback: find settings directory + settings_path = CommonUtils.find_settings_path(self.current_dir) + else: + settings_path = os.path.join(self.current_dir, module_name, "settings") + else: + # Standard or predefined structure + settings_path = CommonUtils.find_settings_path(self.current_dir) + + if not settings_path or not os.path.exists(settings_path): + UIFormatter.print_error("Could not find Django settings directory") + return False + + base_settings_path = os.path.join(settings_path, "base.py") + if not os.path.exists(base_settings_path): + UIFormatter.print_error("Could not find base.py settings file") + return False + + with open(base_settings_path) as f: + content = f.read() + + # Calculate the app module path based on structure + app_module_path = self._calculate_app_module_path(structure_type) + + # Use full AppConfig path to properly support nested apps + # e.g. apps.users -> apps.users.apps.UsersConfig + app_config_name = self.app_name.title().replace("_", "") + full_app_config = f"{app_module_path}.apps.{app_config_name}Config" + + # We'll check if either the module path or the full config path exists + # but we'll prefer adding the full config path + + existing_apps = CommonUtils.extract_existing_apps(content) + + # Check if full config is already there + if full_app_config in existing_apps: + UIFormatter.print_success(f"App '{full_app_config}' already configured in USER_DEFINED_APPS") + return True + + # Check if legacy module path is there, if so, upgrade it + if app_module_path in existing_apps: + UIFormatter.print_info(f"Upgrading app configuration from '{app_module_path}' to '{full_app_config}'...") + updated_content = CommonUtils.replace_app_in_user_defined_apps(content, app_module_path, full_app_config) + + if updated_content: + with open(base_settings_path, "w") as f: + f.write(updated_content) + UIFormatter.print_success(f"Upgraded '{app_module_path}' to '{full_app_config}' in USER_DEFINED_APPS") + return True + else: + UIFormatter.print_error(f"Failed to upgrade app '{app_module_path}'") + return False + + if "USER_DEFINED_APPS" not in content: + UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py") + return False + + updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, [full_app_config]) + if not updated_content: + return False + + with open(base_settings_path, "w") as f: + f.write(updated_content) + + UIFormatter.print_success(f"Added '{full_app_config}' to USER_DEFINED_APPS in base.py") + return True + + def _get_project_structure(self) -> tuple[bool, Optional[str], str]: + """Get project structure with caching to avoid repeated detection.""" + if self._project_structure_cache is None: + if self.config: + apps_config = self.config.get("apps", {}) + nested = apps_config.get("nested", False) + nested_dir = apps_config.get("nested_dir") + + # Determine base dir for apps + if nested and nested_dir: + apps_base_dir = os.path.join(self.current_dir, nested_dir) + else: + apps_base_dir = self.current_dir + + self._project_structure_cache = (nested, nested_dir, apps_base_dir) + else: + # Fallback to old detection method + project_dir, settings_base_path = CommonUtils.find_project_dir(self.current_dir) + + if project_dir is None: + self._project_structure_cache = (False, None, self.current_dir) + else: + self._project_structure_cache = CommonUtils.detect_nested_structure_from_settings( + settings_base_path, self.current_dir + ) + return self._project_structure_cache + + def _is_predefined_structure(self) -> bool: + if self.config: + return self.config.get("structure", {}).get("predefined", False) + + apps_dir = os.path.join(self.current_dir, "apps") + api_dir = os.path.join(self.current_dir, "api") + return os.path.isdir(apps_dir) and os.path.isdir(api_dir) + + def _is_restricted_structure(self) -> bool: + """Check if the project structure is Unified or Single Folder.""" + if self.config: + structure = self.config.get("structure", {}) + return structure.get("unified", False) or structure.get("single", False) + + # Check for Unified structure (apps/api exists) + if os.path.isdir(os.path.join(self.current_dir, "apps", "api")): + return True + + # Check for Predefined structure (apps/ exists but not apps/api) + if os.path.isdir(os.path.join(self.current_dir, "apps")): + return False + + # Check for Single Folder structure (models/ and api/ exist in project dir) + project_dir, _ = CommonUtils.find_project_dir(self.current_dir) + if project_dir: + has_models = os.path.isdir(os.path.join(project_dir, "models")) + has_api = os.path.isdir(os.path.join(project_dir, "api")) + if has_models and has_api: + return True + + return False + + def _create_predefined_app(self, apps_dir: str) -> bool: + """Create an app following the predefined structure with standard Django files.""" + app_module = f"apps.{self.app_name}" + + # Use DjangoHelper.startapp to create standard Django app structure + success = DjangoHelper.startapp(self.app_name, apps_dir, app_module) + + if success: + UIFormatter.print_success(f"Created Django app '{self.app_name}' with predefined structure in {apps_dir}") + else: + UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'") + + return success + + def _add_to_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 diff --git a/src/djinit/creators/files.py b/src/djinit/creators/files.py new file mode 100644 index 0000000..6b7ce39 --- /dev/null +++ b/src/djinit/creators/files.py @@ -0,0 +1,428 @@ +""" +File management utilities for djinit. +Handles creation and modification of project files. +""" + +import os + +from djinit.core.base import BaseService +from djinit.templater import template_engine +from djinit.utils.common import CommonUtils + + +class FileCreator(BaseService): + def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict): + super().__init__(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 CommonUtils.change_cwd(target_dir): + content = template_engine.render_template(template_path, context) + CommonUtils.create_file_with_content(filepath, content, message, should_format=should_format) + + def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None: + """Helper to create multiple folders and files from a specification dict.""" + base_context = base_context or {} + + for folder_name, file_templates in folder_specs.items(): + folder_path = os.path.join(base_dir, folder_name) + CommonUtils.create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py") + + for filename, template_path in file_templates: + file_path = os.path.join(folder_path, filename) + CommonUtils.create_file_from_template(file_path, template_path, base_context, f"Created {file_path}") + + def _create_apps_py( + self, app_dir: str, app_name: str, app_module: str = None, message: str = None, should_format: bool = False + ) -> None: + """Helper to create apps.py file for an app.""" + apps_py_path = os.path.join(app_dir, "apps.py") + app_config_name = app_name.title().replace("_", "") + context = { + "app_name": app_name, + "app_config_name": app_config_name, + "app_module": app_module or app_name, + } + msg = message or f"Created {os.path.basename(app_dir)}/apps.py" + CommonUtils.create_file_from_template( + apps_py_path, "components/apps.py-tpl", context, msg, should_format=should_format + ) + + def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None: + """Helper to create multiple subdirectories with __init__.py files.""" + for subdir in subdirs: + subdir_path = os.path.join(base_dir, subdir) + message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py" + CommonUtils.create_directory_with_init(subdir_path, message) + + def _create_settings_package(self, settings_dir: str, base_context: dict, prefix: str) -> None: + """Helper to create settings package files (base, development, production).""" + from djinit.utils.secretkey import generate_secret_key + + secret_key = generate_secret_key() + dev_context = {"secret_key": secret_key} + + # Prepare context for base.py with full AppConfig paths + base_settings_context = base_context.copy() + if "app_names" in base_settings_context: + transformed_apps = [] + for app in base_settings_context["app_names"]: + # Transform to full AppConfig path: module.path -> module.path.apps.ConfigName + short_name = app.split(".")[-1] + config_name = short_name.title().replace("_", "") + "Config" + # If it's the special "apps" container in unified structure, we might want to skip or handle differently + # But assumes all user apps follow standard djinit structure + full_path = f"{app}.apps.{config_name}" + transformed_apps.append(full_path) + base_settings_context["app_names"] = transformed_apps + + for filename, context in [ + ("base.py", base_settings_context), + ("development.py", dev_context), + ("production.py", base_settings_context), + ]: + filepath = os.path.join(settings_dir, filename) + template_path = f"config/settings/{filename.replace('.py', '')}.py-tpl" + 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 = {"url_type": "project", "api_module": api_module, "include_api": True} + if comment_out_api: + urls_context["comment_out_api_url"] = True + + files = [ + ("urls.py", "config/urls.py-tpl", urls_context), + ("wsgi.py", "config/wsgi.py-tpl", {"project_name": prefix}), + ("asgi.py", "config/asgi.py-tpl", {"project_name": prefix}), + ] + + for filename, template, context in files: + 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-tpl", {}, "Created .gitignore file") + + def create_requirements(self) -> None: + context = { + "use_database_url": self.metadata.get("use_database_url", True), + "database_type": self.metadata.get("database_type", "postgresql"), + } + self._render_and_create_file( + "requirements.txt", + "project/requirements-tpl", + 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.md-tpl", context, "Created README.md file") + + def create_env_file(self) -> None: + """Create .env.sample file with environment variables.""" + context = { + "project_name": self.module_name, + "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-tpl", + context, + "Created .env.sample file with generated secret key", + ) + + def create_pyproject(self, metadata: dict) -> None: + package_name = metadata.get("package_name", "backend") + package_name = CommonUtils.get_package_name(package_name) + context = { + "package_name": package_name, + "project_name": self.project_name, + "database_type": metadata.get("database_type", "postgresql"), + } + self._render_and_create_file( + "pyproject.toml", + "project/pyproject.toml-tpl", + context, + "Created pyproject.toml file for uv", + ) + + def create_project_urls(self) -> None: + """Create project urls.py using project_config/urls.py-tpl template (replaces update_project_urls).""" + effective_app_modules = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata) + context = { + "url_type": "project", + "project_name": self.project_name, + "app_names": effective_app_modules, + "include_apps": True, + "include_api": True, + } + self._render_and_create_file( + "urls.py", + "config/urls.py-tpl", + 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-tpl", + context, + "Created justfile with Django development tasks", + ) + + def create_predefined_structure(self) -> None: + """Create predefined structure with folder organization only (no example code).""" + apps_dir = os.path.join(self.project_root, "apps") + CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py") + + # Create core app with empty subfolders (no example utilities/middleware) + core_dir = os.path.join(apps_dir, "core") + CommonUtils.create_directory_with_init(core_dir, "Created apps/core/__init__.py") + + # Create empty folders for utilities, mixins, middleware (developers add their own) + core_subfolders = ["utils", "mixins", "middleware"] + self._create_subdirectories_with_init(core_dir, core_subfolders, "apps/core/") + + # Create api directory structure + api_dir = os.path.join(self.project_root, "api") + CommonUtils.create_directory_with_init(api_dir, "Created api/__init__.py") + api_urls_path = os.path.join(api_dir, "urls.py") + CommonUtils.create_file_from_template( + api_urls_path, + "config/urls.py-tpl", + {"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "api"}, + "Created api/urls.py", + ) + + api_v1_dir = os.path.join(api_dir, "v1") + CommonUtils.create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py") + api_v1_urls_path = os.path.join(api_v1_dir, "urls.py") + CommonUtils.create_file_from_template( + api_v1_urls_path, + "config/urls.py-tpl", + {"url_type": "api_version", "app_name": "v1", "app_list": [], "app_module": "apps"}, + "Created api/v1/urls.py", + ) + + project_urls_path = os.path.join(self.project_configs, "urls.py") + CommonUtils.create_file_from_template( + project_urls_path, + "config/urls.py-tpl", + {"url_type": "project", "api_module": "api", "include_api": True}, + "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") + CommonUtils.create_directory_with_init(core_dir, "Created core/__init__.py") + + settings_dir = os.path.join(core_dir, "settings") + CommonUtils.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") + CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py") + + # Create apps.py for the 'apps' app + self._create_apps_py(apps_dir, "apps", app_module="apps", message="Created apps/apps.py", should_format=True) + + # Create sub-components as directories with __init__.py + components = ["admin", "models", "serializers", "tests", "urls", "views"] + self._create_subdirectories_with_init(apps_dir, components, "apps/") + + # Create 'api' directory with v1 + api_dir = os.path.join(apps_dir, "api") + CommonUtils.create_directory_with_init(api_dir, "Created apps/api/__init__.py") + + CommonUtils.create_file_from_template( + os.path.join(api_dir, "urls.py"), + "config/urls.py-tpl", + {"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "apps.api"}, + "Created apps/api/urls.py", + should_format=True, + ) + + api_v1_dir = os.path.join(api_dir, "v1") + CommonUtils.create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py") + + CommonUtils.create_file_from_template( + os.path.join(api_v1_dir, "urls.py"), + "config/urls.py-tpl", + {"url_type": "api_version", "app_name": "v1", "app_list": [], "app_module": "apps"}, + "Created apps/api/v1/urls.py", + should_format=True, + ) + + def create_single_structure(self) -> None: + """Create single folder structure with minimal files (no example code).""" + project_dir = os.path.join(self.project_root, self.module_name) + CommonUtils.create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py") + + settings_dir = os.path.join(project_dir, "settings") + CommonUtils.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 + ) + + # Create component folders with __init__.py only (no example code) + components = ["admin", "api", "models", "tests"] + self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/") + + # Create README files for guidance + CommonUtils.create_file_from_template( + os.path.join(project_dir, "api", "README.md"), + "components/api_readme.md-tpl", + {"module_name": self.module_name}, + f"Created {self.module_name}/api/README.md", + ) + + CommonUtils.create_file_from_template( + os.path.join(project_dir, "models", "README.md"), + "components/models_readme.md-tpl", + {"module_name": self.module_name}, + f"Created {self.module_name}/models/README.md", + ) + + def create_procfile(self) -> None: + context = {"project_name": self.project_name} + self._render_and_create_file( + "Procfile", + "project/procfile-tpl", + 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-tpl", + 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-tpl", + 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-tpl", + 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") + CommonUtils.create_file_with_content( + filepath, json.dumps(config, indent=4), "Created .djinit configuration file" + ) diff --git a/src/djinit/creators/project.py b/src/djinit/creators/project.py new file mode 100644 index 0000000..b0a663b --- /dev/null +++ b/src/djinit/creators/project.py @@ -0,0 +1,201 @@ +""" +Project management for djinit. +Handles creation of Django projects and apps. +""" + +import os + +from djinit.core.base import BaseService +from djinit.ui.console import UIFormatter +from djinit.utils.common import CommonUtils +from djinit.utils.django import DjangoHelper + + +class ProjectCreator(BaseService): + def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict): + super().__init__(project_root=os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)) + self.project_dir = project_dir + self.project_name = project_name + self.app_names = app_names + self.metadata = metadata + # self.project_root is set by BaseService via super().__init__ but we need to ensure it matches logic + # logic was: self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir) + # which I passed to super. So I can remove the line setting self.project_root here. + self.module_name = metadata.get("project_module_name") or self.project_name + + def create_project(self) -> None: + os.makedirs(self.project_root, exist_ok=True) + + unified = self.metadata.get("unified_structure", False) + DjangoHelper.startproject(self.module_name, self.project_root, unified=unified) + 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: + CommonUtils.create_directory_with_init( + apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py" + ) + + # Calculate module paths for correct AppConfig generation (e.g. src.users) + app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata) + + for i, app_name in enumerate(self.app_names): + app_module = app_module_paths[i] + DjangoHelper.startapp(app_name, apps_base_dir, app_module) + UIFormatter.print_success(f"Django app '{app_name}' created successfully!") + + self.add_apps_to_settings() + + def add_apps_to_settings(self) -> None: + """Add all apps to USER_DEFINED_APPS in base.py settings file.""" + base_settings_path = CommonUtils.get_base_settings_path(self.project_root, self.module_name) + + if not os.path.exists(base_settings_path): + from djinit.utils.exceptions import ConfigError + + raise ConfigError("Could not find base.py settings file") + + content = CommonUtils.read_base_settings(self.project_root, self.module_name) + if content is None: + from djinit.utils.exceptions import ConfigError + + raise ConfigError("Could not read base.py settings file") + + app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata) + + # Transform to full AppConfig paths (e.g. apps.users.apps.UsersConfig) + full_config_paths = [] + for app in app_module_paths: + short_name = app.split(".")[-1] + config_name = short_name.title().replace("_", "") + "Config" + full_config_paths.append(f"{app}.apps.{config_name}") + + existing_apps = CommonUtils.extract_existing_apps(content) + + # Check against both module path and full config path to avoid duplicates + apps_to_add = [] + for i, full_path in enumerate(full_config_paths): + mod_path = app_module_paths[i] + if full_path not in existing_apps and mod_path not in existing_apps: + apps_to_add.append(full_path) + elif mod_path in existing_apps: + # If legacy module path exists, we could upgrade it here, + # but ProjectManager is typically initialized on creation. + # For now, let's just not add a duplicate. + # Actually, let's upgrade it if we can + pass + + # If we found apps to add, insert them + if not apps_to_add: + # Check if we need to upgrade any legacy paths + upgraded = False + for i, full_path in enumerate(full_config_paths): + mod_path = app_module_paths[i] + if mod_path in existing_apps and full_path not in existing_apps: + content = CommonUtils.replace_app_in_user_defined_apps(content, mod_path, full_path) + upgraded = True + + if upgraded: + with open(base_settings_path, "w") as f: + f.write(content) + UIFormatter.print_success("Upgraded existing apps to full AppConfig paths") + return + + UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS") + return + + updated_content = CommonUtils.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: + """Validate project structure - only checks essential configuration files.""" + join = lambda *args: os.path.join(self.project_root, *args) + + # Core Django project files + required_files = [ + join("manage.py"), + join(self.module_name, "__init__.py"), + 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"): + # Standard structure - validate all standard Django app files + for app_name in self.app_names: + app_path = lambda f, app=app_name: os.path.join(apps_base_dir, app, f) + app_files = [ + app_path("__init__.py"), + app_path("apps.py"), + app_path("admin.py"), + app_path("models.py"), + app_path("views.py"), + app_path("serializers.py"), + app_path("urls.py"), + app_path("tests.py"), + app_path("migrations"), + ] + 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}") diff --git a/src/djinit/creators/setup.py b/src/djinit/creators/setup.py new file mode 100644 index 0000000..ac44f53 --- /dev/null +++ b/src/djinit/creators/setup.py @@ -0,0 +1,120 @@ +import os +from typing import Callable, List, Tuple + +from djinit.creators.files import FileCreator +from djinit.creators.project import ProjectCreator +from djinit.ui.console import UIFormatter + + +class SetupCreator: + """ + Orchestrator for creating a complete Django project. + Combines ProjectCreator and FileCreator to set up the environment. + """ + + def __init__(self, project_dir: str, project_name: str, primary_app: str, app_names: list, metadata: dict): + self.project_dir = project_dir + self.project_name = project_name + self.primary_app = primary_app + self.app_names = app_names + self.metadata = metadata + if project_dir == ".": + self.project_root = os.getcwd() + else: + self.project_root = os.path.join(os.getcwd(), project_dir) + + self.project_creator = ProjectCreator(project_dir, project_name, app_names, metadata) + self.file_creator = FileCreator(self.project_root, project_name, app_names, metadata) + + def _normalize_metadata(self) -> None: + """Normalize metadata and app names based on structure type.""" + 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_creator.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_creator.app_names = self.app_names + + elif self.metadata.get("single_structure"): + self.metadata.setdefault("nested_apps", False) + if not self.app_names: + self.app_names = [] + self.project_creator.app_names = self.app_names + + def create(self) -> bool: + """Run the full setup process.""" + self._normalize_metadata() + + steps: List[Tuple[str, Callable[[], None]]] = [] + + steps.append(("Creating Django project", self.project_creator.create_project)) + + if self.metadata.get("unified_structure"): + steps.append(("Creating unified structure", self.file_creator.create_unified_structure)) + elif self.metadata.get("single_structure"): + steps.append(("Creating single folder structure", self.file_creator.create_single_structure)) + elif self.metadata.get("predefined_structure"): + steps.append(("Creating predefined structure", self.file_creator.create_predefined_structure)) + steps.append(("Adding apps to settings", self.project_creator.add_apps_to_settings)) + else: + steps.append(("Creating Django apps", self.project_creator.create_apps)) + steps.append(("Creating project URLs", self.file_creator.create_project_urls)) + + steps.extend( + [ + ("Validating project structure", self.project_creator.validate_project_structure), + ("Creating utility files", self._create_utility_files), + ("Creating Procfile", self.file_creator.create_procfile), + ("Creating Justfile", self.file_creator.create_justfile), + ("Creating runtime.txt", self.file_creator.create_runtime_txt), + ("Creating CI/CD pipelines", self._create_cicd_pipelines), + ] + ) + + total_steps = len(steps) + success = True + + progress, task = UIFormatter.create_live_progress(description="Setup Progress", total_steps=total_steps) + + 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}") + + except Exception as e: + success = False + UIFormatter.handle_exception(e) + + return success + + def _create_utility_files(self) -> None: + utility_steps = [ + self.file_creator.create_gitignore, + self.file_creator.create_requirements, + self.file_creator.create_readme, + self.file_creator.create_env_file, + self.file_creator.create_djinit_config, + lambda: self.file_creator.create_pyproject(self.metadata), + ] + + for step_func in utility_steps: + step_func() + + UIFormatter.print_success("Created all utility files successfully!") + + def _create_cicd_pipelines(self) -> None: + if self.metadata.get("use_github_actions", True): + self.file_creator.create_github_actions() + + if self.metadata.get("use_gitlab_ci", True): + self.file_creator.create_gitlab_ci() diff --git a/src/djinit/templater/__init__.py b/src/djinit/templater/__init__.py new file mode 100644 index 0000000..6df7d13 --- /dev/null +++ b/src/djinit/templater/__init__.py @@ -0,0 +1,89 @@ +""" +Template engine for djinit. +Handles loading and rendering Jinja2 templates for file generation. +""" + +import os +from pathlib import Path +from typing import Any, Dict + +from djinit.core.base import BaseService +from djinit.core.parser import InFileLogicParser + + +class TemplateEngine(BaseService): + """Template engine for rendering file templates.""" + + def __init__(self, template_dir: str = None): + """Initialize template engine. + + Args: + template_dir: Path to templates directory. If None, uses default. + """ + if template_dir is None: + # djinit/templater/__init__.py -> djinit/templater -> djinit -> templates + template_dir = Path(__file__).resolve().parent.parent / "templates" + + super().__init__() + + if not template_dir.exists(): + raise FileNotFoundError(f"Template directory not found: {template_dir}") + + self.template_dir = str(template_dir) + self.parser = InFileLogicParser() + + def render_template(self, template_name: str, context: Dict[str, Any]) -> str: + """Render a template with the given context. + + Args: + template_name: Name of the template file (e.g., 'gitignore.py-tpl') + context: Dictionary of variables to pass to the template + + Returns: + Rendered template content as string + """ + template_path = os.path.join(self.template_dir, template_name) + if not os.path.exists(template_path): + raise FileNotFoundError(f"Template not found: {template_path}") + + with open(template_path) as f: + template_text = f.read() + + return self.parser.render(template_text, context) + + def render_string(self, template_string: str, context: Dict[str, Any]) -> str: + """Render a template string with the given context. + + Args: + template_string: Template content as string + context: Dictionary of variables to pass to the template + + Returns: + Rendered template content as string + """ + return self.parser.render(template_string, context) + + def get_template_names(self) -> list[str]: + """Get list of available template files.""" + if not os.path.exists(self.template_dir): + return [] + + templates = [] + for root, _, files in os.walk(self.template_dir): + for file in files: + if file.endswith("-tpl"): + rel_path = os.path.relpath(os.path.join(root, file), self.template_dir) + templates.append(rel_path) + return sorted(templates) + + def template_exists(self, template_name: str) -> bool: + """Check if a template file exists. + + Args: + template_name: Name of the template file + """ + template_path = os.path.join(self.template_dir, template_name) + return os.path.exists(template_path) + + +template_engine = TemplateEngine() diff --git a/src/djinit/templater/base.py b/src/djinit/templater/base.py new file mode 100644 index 0000000..c5c28d3 --- /dev/null +++ b/src/djinit/templater/base.py @@ -0,0 +1,75 @@ +import logging +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from djinit.templater import template_engine + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class TemplateFile: + """A template file to be rendered. + + The template file extension should end with '-tpl'. + """ + + path: Path + context: dict[str, Any] = field(default_factory=dict) + template_name: str | None = None # Optional override, otherwise derived from path name + -tpl + + +def create_file( + template_file: TemplateFile, + template_name: str | None = None, +) -> None: + """Create file based on a template. + + Args: + template_file: The TemplateFile object containing path and context. + template_name: Optional explicit template name. If not provided, + it looks for template_file.template_name, + or defaults to template_file.path.name + "-tpl". + """ + path = template_file.path + + logger.debug(f"Create file, {path}, if it doesn't exist") + + # Ensure parent directory exists + if not path.parent.exists(): + path.parent.mkdir(parents=True, exist_ok=True) + # Create __init__.py in parents if needed? + # CommonUtils.create_directory_with_init handles this usually, + # but here we are being more granular. + # For now, let's assume specific directories are created or we blindly create parents. + # But wait, django-new logic creates parents. + + if path.exists(): + # logic to overwrite or skip? django-new skips if exists usually, + # or maybe we want to force? + # For now, let's follow django-new: + logger.debug(f"Do not create template file, {path}, because it already exists") + # However, djinit usually overwrites or at least proceeds. + # Let's keep it safe. + pass + # Actually, let's overwrite if needed or warn. + # But CommonUtils.create_file_from_template overwrites. + # Let's stick to djinit behavior of overwriting/creating content. + + # Check explicitly passed template_name -> template_file.template_name -> path.name + "-tpl" + tpl_name = template_name or template_file.template_name or (path.name + "-tpl") + + try: + content = template_engine.render_template(tpl_name, template_file.context) + + with open(path, "w") as f: + f.write(content) + + logger.debug(f"Created template file, {path}") + # We might want to print success via UIFormatter, but maybe keep this low level. + + except FileNotFoundError: + # Fallback or error + logger.error(f"Template {tpl_name} not found.") + raise diff --git a/src/djinit/templates/components/admin.py-tpl b/src/djinit/templates/components/admin.py-tpl new file mode 100644 index 0000000..dc9bfc6 --- /dev/null +++ b/src/djinit/templates/components/admin.py-tpl @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.contrib import admin + +# Register your models here. diff --git a/src/djinit/templates/components/api_readme.md-tpl b/src/djinit/templates/components/api_readme.md-tpl new file mode 100644 index 0000000..5d416ff --- /dev/null +++ b/src/djinit/templates/components/api_readme.md-tpl @@ -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 +└── / # 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 diff --git a/src/djinit/templates/components/apps.py-tpl b/src/djinit/templates/components/apps.py-tpl new file mode 100644 index 0000000..fdac009 --- /dev/null +++ b/src/djinit/templates/components/apps.py-tpl @@ -0,0 +1,12 @@ +from django.apps import AppConfig + + + +class [[ app_config_name ]]Config(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' +# @IF app_module + name = '[[ app_module ]]' +# @ENDIF +# @IF not app_module + name = '[[ app_name ]]' +# @ENDIF diff --git a/src/djinit/templates/components/models.py-tpl b/src/djinit/templates/components/models.py-tpl new file mode 100644 index 0000000..980c8e5 --- /dev/null +++ b/src/djinit/templates/components/models.py-tpl @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.db import models + +# Create your models here. diff --git a/src/djinit/templates/components/models_readme.md-tpl b/src/djinit/templates/components/models_readme.md-tpl new file mode 100644 index 0000000..0bf247c --- /dev/null +++ b/src/djinit/templates/components/models_readme.md-tpl @@ -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() +``` diff --git a/src/djinit/templates/components/serializers.py-tpl b/src/djinit/templates/components/serializers.py-tpl new file mode 100644 index 0000000..600d85c --- /dev/null +++ b/src/djinit/templates/components/serializers.py-tpl @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import serializers + +# Create your serializers here. diff --git a/src/djinit/templates/components/tests.py-tpl b/src/djinit/templates/components/tests.py-tpl new file mode 100644 index 0000000..7fdd344 --- /dev/null +++ b/src/djinit/templates/components/tests.py-tpl @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.test import TestCase + +# Create your tests here. diff --git a/src/djinit/templates/components/urls.py-tpl b/src/djinit/templates/components/urls.py-tpl new file mode 100644 index 0000000..4d77864 --- /dev/null +++ b/src/djinit/templates/components/urls.py-tpl @@ -0,0 +1,7 @@ +"""generated with djinit""" + +from django.urls import path + +urlpatterns = [ + # Add your URL patterns here +] diff --git a/src/djinit/templates/components/views.py-tpl b/src/djinit/templates/components/views.py-tpl new file mode 100644 index 0000000..e67469e --- /dev/null +++ b/src/djinit/templates/components/views.py-tpl @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import viewsets + +# Create your views here. diff --git a/src/djinit/templates/config/asgi.py-tpl b/src/djinit/templates/config/asgi.py-tpl new file mode 100644 index 0000000..0c5c879 --- /dev/null +++ b/src/djinit/templates/config/asgi.py-tpl @@ -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="[[ project_name ]].settings.production")) +application = get_asgi_application() diff --git a/src/djinit/templates/config/settings/base.py-tpl b/src/djinit/templates/config/settings/base.py-tpl new file mode 100644 index 0000000..ccdb901 --- /dev/null +++ b/src/djinit/templates/config/settings/base.py-tpl @@ -0,0 +1,180 @@ +""" +generated with djinit +Common settings shared between development and production environment +""" + +import os +from datetime import timedelta +from pathlib import Path + +import environ + +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 + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + + +# Application definition +THIRD_PARTY_APPS = [ + "rest_framework", + "rest_framework_simplejwt", + "rest_framework_simplejwt.token_blacklist", + "corsheaders", + "drf_spectacular", +] + +USER_DEFINED_APPS = [ + # @LOOP app in app_names + "[[ app ]]", + # @ENDLOOP +] + +BUILT_IN_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +INSTALLED_APPS = BUILT_IN_APPS + THIRD_PARTY_APPS + USER_DEFINED_APPS +INSTALLED_APPS.insert(0, "jazzmin") + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "[[ project_name ]].urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "[[ project_name ]].wsgi.application" + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True + +# Static files and media files +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "staticfiles" + +MEDIA_URL = "media/" +MEDIA_ROOT = BASE_DIR / "media" + +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# REST Framework Settings +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + ], + "DEFAULT_PARSER_CLASSES": [ + "rest_framework.parsers.JSONParser", + ], + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + "rest_framework.authentication.SessionAuthentication", + ), + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 20, +} + +# DRF Spectacular settings +SPECTACULAR_SETTINGS = { + "TITLE": "[[ project_name ]] API", + "DESCRIPTION": "API documentation for [[ project_name ]]", + "VERSION": "1.0.0", + "SERVE_INCLUDE_SCHEMA": False, + "COMPONENT_SPLIT_REQUEST": True, + "SCHEMA_PATH_PREFIX": "/api/", +} + +# JWT Settings +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(days=1), + "REFRESH_TOKEN_LIFETIME": timedelta(days=3), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, +} + +# CORS Settings +CORS_ALLOW_METHODS = [ + "DELETE", + "GET", + "OPTIONS", + "PATCH", + "POST", + "PUT", +] + +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 +X_FRAME_OPTIONS = "DENY" diff --git a/src/djinit/templates/config/settings/development.py-tpl b/src/djinit/templates/config/settings/development.py-tpl new file mode 100644 index 0000000..2e3c1b3 --- /dev/null +++ b/src/djinit/templates/config/settings/development.py-tpl @@ -0,0 +1,42 @@ +"""generated with djinit""" + +from .base import * + +SECRET_KEY = "[[ secret_key ]]" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ["*"] + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +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; + """, + }, + } +} + +# Email backend for development +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 diff --git a/src/djinit/templates/config/settings/production.py-tpl b/src/djinit/templates/config/settings/production.py-tpl new file mode 100644 index 0000000..7d58363 --- /dev/null +++ b/src/djinit/templates/config/settings/production.py-tpl @@ -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' diff --git a/src/djinit/templates/config/urls.py-tpl b/src/djinit/templates/config/urls.py-tpl new file mode 100644 index 0000000..574294f --- /dev/null +++ b/src/djinit/templates/config/urls.py-tpl @@ -0,0 +1,103 @@ +# @IF url_type == 'project' +"""generated with djinit""" + +from django.contrib import admin +from django.urls import path +# @IF include_api or include_apps +from django.urls import include +# @ENDIF +# @IF include_api +from django.conf import settings +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularSwaggerView, +) +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenBlacklistView, +) +# @ENDIF + +urlpatterns = [ + path("admin/", admin.site.urls), +# @IF include_api + # JWT tokens + path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"), +# @ENDIF +# @IF include_api and comment_out_api_url + # path("api/", include("[[ api_module ]].urls")), +# @ENDIF +# @IF include_api and not comment_out_api_url + path("api/", include("[[ api_module ]].urls")), +# @ENDIF +# @IF include_apps + # App URLs +# @LOOP app in app_names + path("", include("[[ app ]].urls")), +# @ENDLOOP +# @ENDIF +] +# @IF include_api + +# Don't show schema in production +if settings.DEBUG: + urlpatterns += [ + path("schema/", SpectacularAPIView.as_view(), name="schema"), + path("docs/", SpectacularSwaggerView.as_view(url_name="schema")), + ] +# @ENDIF +# @ENDIF +# @IF url_type == 'api_root' +from django.urls import include, path + +app_name = "[[ app_name ]]" + +urlpatterns = [ + path("[[ version ]]/", include("[[ api_module ]].[[ version ]].urls")), +] +# @ENDIF +# @IF url_type == 'api_version' +from django.urls import include, path + +app_name = "[[ app_name ]]" + +urlpatterns = [ +# @LOOP app in app_list + path("[[ app ]]/", include("[[ app_module ]].[[ app ]].urls")), +# @ENDLOOP +] +# @ENDIF +# @IF url_type == 'drf_router' +from rest_framework.routers import DefaultRouter +# @IF has_viewset +from [[ app_module ]].views.[[ view_file ]] import [[ viewset_name ]] +# @ENDIF +# @IF has_app_name + +app_name = "[[ app_name ]]" +# @ENDIF + +router = DefaultRouter() +# @IF has_viewset +router.register(r"[[ router_prefix ]]", [[ viewset_name ]], basename="[[ basename ]]") +# @ENDIF + +urlpatterns = [ +# @IF has_include + path("", include(router.urls)), +# @ENDIF +] +# @IF not has_include +urlpatterns = router.urls +# @ENDIF +# @ENDIF +# @IF url_type == 'component' +from django.urls import path + +urlpatterns = [ + # Add your URL patterns here +] +# @ENDIF diff --git a/src/djinit/templates/config/wsgi.py-tpl b/src/djinit/templates/config/wsgi.py-tpl new file mode 100644 index 0000000..421456d --- /dev/null +++ b/src/djinit/templates/config/wsgi.py-tpl @@ -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="[[ project_name ]].settings.production")) + +application = get_wsgi_application() diff --git a/src/djinit/templates/project/ci/github_actions-tpl b/src/djinit/templates/project/ci/github_actions-tpl new file mode 100644 index 0000000..5217107 --- /dev/null +++ b/src/djinit/templates/project/ci/github_actions-tpl @@ -0,0 +1,109 @@ +name: Django CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12", "3.13"] + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: test_db + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python {{ '{{ matrix.python-version }}' }} + uses: actions/setup-python@v5 + with: + python-version: {{ '{{ matrix.python-version }}' }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Install dependencies + run: uv sync + + - name: Run linting + run: uv run ruff check . + + - name: Format check + run: uv run ruff format --check . + + - name: Set up PostgreSQL + env: + DB_NAME: test_db + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: localhost + DB_PORT: 5432 + run: | + echo "PostgreSQL is ready" + + - name: Run Django tests + env: + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + DB_NAME: test_db + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: localhost + DB_PORT: 5432 + run: | + uv run python manage.py check + uv run python manage.py test + + - name: Check migrations + env: + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + DB_NAME: test_db + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: localhost + DB_PORT: 5432 + run: | + uv run python manage.py makemigrations --check --dry-run + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Install dependencies + run: uv sync + + - name: Run security check + run: uv run python manage.py check --deploy diff --git a/src/djinit/templates/project/ci/gitlab_ci-tpl b/src/djinit/templates/project/ci/gitlab_ci-tpl new file mode 100644 index 0000000..3d3bc61 --- /dev/null +++ b/src/djinit/templates/project/ci/gitlab_ci-tpl @@ -0,0 +1,133 @@ +# GitLab CI/CD configuration for [[ project_name ]] + +stages: + - test + - lint + - security + +variables: + POSTGRES_DB: test_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/test_db" + +# Use Docker-in-Docker +services: + - postgres:15 + +before_script: + - python --version + - pip install uv + - uv sync + +# Lint stage +lint: + stage: lint + image: python:3.13 + script: + - uv run ruff check . + - uv run ruff format --check . + only: + - branches + - merge_requests + +# Test stage +test: + stage: test + image: python:3.13 + variables: + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + script: + - uv run python manage.py check + - uv run python manage.py migrate + - uv run python manage.py test + only: + - branches + - merge_requests + +# Multiple Python versions +test:python310: + stage: test + image: python:3.10 + variables: + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + script: + - pip install uv + - uv sync + - uv run python manage.py check + - uv run python manage.py migrate + - uv run python manage.py test + only: + - branches + - merge_requests + +test:python311: + stage: test + image: python:3.11 + variables: + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + script: + - pip install uv + - uv sync + - uv run python manage.py check + - uv run python manage.py migrate + - uv run python manage.py test + only: + - branches + - merge_requests + +test:python312: + stage: test + image: python:3.12 + variables: + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + script: + - pip install uv + - uv sync + - uv run python manage.py check + - uv run python manage.py migrate + - uv run python manage.py test + only: + - branches + - merge_requests + +test:python313: + stage: test + image: python:3.13 + variables: + DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + script: + - pip install uv + - uv sync + - uv run python manage.py check + - uv run python manage.py migrate + - uv run python manage.py test + only: + - branches + - merge_requests + +# Security check +security: + stage: security + image: python:3.13 + variables: + DJANGO_SETTINGS_MODULE: [[ project_name ]].settings.development + SECRET_KEY: test-secret-key-for-ci + script: + - pip install uv + - uv sync + - uv run python manage.py check --deploy + - uv run python manage.py validate_templates + only: + - main + - develop diff --git a/src/djinit/templates/project/env_sample-tpl b/src/djinit/templates/project/env_sample-tpl new file mode 100644 index 0000000..382da28 --- /dev/null +++ b/src/djinit/templates/project/env_sample-tpl @@ -0,0 +1,32 @@ +# Django settings +DJANGO_SETTINGS_MODULE=[[ project_name ]].settings.development +SECRET_KEY= your-secret-key +ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com + +# @IF use_database_url +# Database URL (recommended for production) +# @IF database_type == 'mysql' +DATABASE_URL=mysql://user:password@localhost:3306/[[ project_name ]] +# @ELSE +DATABASE_URL=postgres://user:password@localhost:5432/[[ project_name ]] +# @ENDIF +# @ELSE +# Database (individual parameters) +DB_NAME= +DB_USER= +DB_PASSWORD= +DB_HOST= +DB_PORT= +# @ENDIF + +# Frontend URL +FRONTEND_URL=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= diff --git a/src/djinit/templates/project/gitignore-tpl b/src/djinit/templates/project/gitignore-tpl new file mode 100644 index 0000000..2b5243c --- /dev/null +++ b/src/djinit/templates/project/gitignore-tpl @@ -0,0 +1,93 @@ +#--------------------------------------------------# +# The following was generated by djinit: # +#--------------------------------------------------# + +# Python +*.py[cod] +*$py.class +*.so +__pycache__/ +*.egg-info/ +dist/ +build/ +.eggs/ +*.egg + +# Django +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +*.sqlite3 + +# Environment +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ +.project +.pydevproject +.settings/ +.ropeproject/ + +# OS +.DS_Store +*.DS_Store +Thumbs.db +desktop.ini + +# Django specific +media/ +staticfiles/ +staticfiles/ +static_root/ +/static/ + +# Testing +.coverage +htmlcov/ +.pytest_cache/ +.tox/ +coverage.xml +*.cover +.hypothesis/ +.cache + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Node modules (if using frontend tools) +node_modules/ + +# Secret keys +secret.txt +secrets.json + +# Jupyter Notebook +.ipynb_checkpoints + +# Backup files +*.bak +*.tmp +*~ + +# Database +*.db +*.sql + +# Cloud +.cloud diff --git a/src/djinit/templates/project/init.py-tpl b/src/djinit/templates/project/init.py-tpl new file mode 100644 index 0000000..e69de29 diff --git a/src/djinit/templates/project/justfile-tpl b/src/djinit/templates/project/justfile-tpl new file mode 100644 index 0000000..127e77d --- /dev/null +++ b/src/djinit/templates/project/justfile-tpl @@ -0,0 +1,219 @@ +# Justfile for [[ project_name ]] +# Task runner for Django development +# Install just: https://github.com/casey/just + +# Default task: show available commands +default: + @just --list + +# Run development server +dev: + uv run python manage.py runserver + +# 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{{ '}}' }} + +# Create migrations for all apps +makemigrations: + uv run python manage.py makemigrations + +# Create migrations for specific app +makemigrations-app APP: + uv run python manage.py makemigrations {{ '{{' }}APP{{ '}}' }} + +# Run all migrations +migrate: + uv run python manage.py migrate + +# Reset database (delete all migrations) +reset-db: + uv run python manage.py flush --noinput + uv run python manage.py migrate + +# Create superuser +createsuperuser: + uv run python manage.py createsuperuser + +# Collect static files +collectstatic: + uv run python manage.py collectstatic --noinput + +# Run Django shell +shell: + uv run python manage.py shell + +# Run Django shell plus (IPython) +shell-plus: + uv run python manage.py shell_plus + +# Run tests +test: + uv run python manage.py test + +# Run tests with coverage +test-coverage: + uv run pytest --cov=. --cov-report=html --cov-report=term + +# Lint code with ruff +lint: + uv run ruff check . + +# Format code with ruff +format: + uv run ruff format . + +# Lint and format code +style: + uv run ruff check . + uv run ruff format . + +# Check security issues +check: + uv run python manage.py check + +# Check security issues (production settings) +check-deploy: + uv run python manage.py check --deploy + +# Show all database migrations status +showmigrations: + uv run python manage.py showmigrations + +# Sync dependencies (uv equivalent of pip freeze) +requirements: + uv pip compile + +# Run all linting and checks +ci: + uv run ruff check . + uv run ruff format --check . + uv run python manage.py check + +# Start interactive shell +shell-ipython: + uv run python manage.py shell_plus + +# Clear all cache +clear-cache: + uv run python manage.py clear_cache + +# Show Django version +version: + uv run python manage.py version + +# Run database shell +dbshell: + uv run python manage.py dbshell + +# Start production server with gunicorn +server: + uv run gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:8000 + +# Start production server with gunicorn workers +server-prod WORKERS="4": + uv run gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:8000 --workers {{ '{{' }}WORKERS{{ '}}' }} + +# Start production server with gunicorn and reload on file changes +server-watch: + uv run gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:8000 --reload + +# Clean Python cache files +clean: + find . -type d -name "__pycache__" -exec rm -rf {} + + find . -type f -name "*.pyc" -delete + find . -type f -name "*.pyo" -delete + find . -type f -name ".coverage" -delete + find . -type d -name "*.egg-info" -exec rm -rf {} + + rm -rf htmlcov/ + rm -rf .pytest_cache/ + +# Pin dependencies in pyproject.toml using versions from uv.lock, keeping >= and extras +pin-deps: + #!/usr/bin/env -S uv run python + import tomllib, re + from pathlib import Path + + lock = Path("uv.lock") + if not lock.exists(): + print("uv.lock not found — run `uv sync` first") + raise SystemExit(0) + + data = tomllib.loads(lock.read_text()) + packages = {p["name"].lower(): p["version"] for p in data.get("package", [])} + + py = Path("pyproject.toml") + lines = py.read_text().splitlines() + + pattern = re.compile(r'^(\s*)"([^"]+)"(,?)') + + out = [] + for line in lines: + m = pattern.match(line) + if not m: + out.append(line) + continue + + indent, dep, comma = m.groups() + + base = dep + extras = "" + operator = ">=" + version = None + + # extras: django[postgres] + if "[" in base and "]" in base: + before, after = base.split("[", 1) + extras = "[" + after.split("]")[0] + "]" + base = before + + # detect >= or == + if ">=" in dep: + operator = ">=" + base, version = dep.split(">=") + base = base.strip() + elif "==" in dep: + operator = "==" + base, version = dep.split("==") + base = base.strip() + else: + base = dep + + base_lower = base.lower() + new_version = packages.get(base_lower) + + if new_version: + new_dep = f'{base}{extras}{operator}{new_version}' + line = f'{indent}"{new_dep}"{comma}' + + out.append(line) + + py.write_text("\n".join(out) + "\n") + print("Pinned dependencies (>= preserved, extras preserved).") + + +# Setup project for development (using uv) +setup: + uv sync + just pin-deps + uv run python manage.py migrate + uv run python manage.py createsuperuser + +# Complete setup including static files +setup-complete: + uv sync + uv run python manage.py migrate + uv run python manage.py collectstatic --noinput + uv run python manage.py createsuperuser + +# Install a new package +install PACKAGE: + uv add {{ '{{' }}PACKAGE{{ '}}' }} + +# Install a dev package +install-dev PACKAGE: + uv add --dev {{ '{{' }}PACKAGE{{ '}}' }} + +# Remove a package +remove PACKAGE: + uv remove {{ '{{' }}PACKAGE{{ '}}' }} diff --git a/src/djinit/templates/project/manage.py-tpl b/src/djinit/templates/project/manage.py-tpl new file mode 100644 index 0000000..ba6f5c7 --- /dev/null +++ b/src/djinit/templates/project/manage.py-tpl @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +"""generated with djinit""" + +"""Django's command-line utility for administrative tasks.""" +import os +import sys + +import environ + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "[[ project_name ]].settings.production")) + + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + env = environ.Env() + # Read .env file, if it exists + environ.Env.read_env() + + main() diff --git a/src/djinit/templates/project/procfile-tpl b/src/djinit/templates/project/procfile-tpl new file mode 100644 index 0000000..fc01178 --- /dev/null +++ b/src/djinit/templates/project/procfile-tpl @@ -0,0 +1,20 @@ +# Procfile for [[ project_name ]] +# Used for deploying to PaaS platforms like Heroku, Railway, Render, etc. + +# Web server process (using Gunicorn) +# Adjust workers based on your needs: (2 x CPU cores) + 1 +web: gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info + +# Alternative web server configurations (uncomment to use): +# web: gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120 +# web: gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120 + +# Release task (runs migrations before deployment) +release: python manage.py migrate --noinput && python manage.py collectstatic --noinput + +# Uncomment below to run scheduled tasks (if using Celery) +# worker: celery -A [[ project_name ]] worker --loglevel=info +# beat: celery -A [[ project_name ]] beat --loglevel=info + +# Development server (for local testing with Foreman/Overmind) +# web: python manage.py runserver 0.0.0.0:$PORT diff --git a/src/djinit/templates/project/pyproject.toml-tpl b/src/djinit/templates/project/pyproject.toml-tpl new file mode 100644 index 0000000..e9e49a3 --- /dev/null +++ b/src/djinit/templates/project/pyproject.toml-tpl @@ -0,0 +1,46 @@ +[project] +name = "[[ package_name ]]" +version = "0.1.0" +description = "" +readme = "README.md" +requires-python = ">=3.13" +license = { text = "MIT" } +dependencies = [ + "django", + "django-environ", + "django-jazzmin", + "djangorestframework", + "djangorestframework-simplejwt", + "drf-spectacular", + "django-cors-headers", + "whitenoise", +# @IF database_type == 'mysql' + "mysqlclient", +# @ENDIF +# @IF database_type == 'postgresql' + "psycopg2-binary", +# @ENDIF + "gunicorn", +] + +[tool.ruff] +line-length = 88 +target-version = "py38" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long, handled by formatter + "B008", # do not perform function calls in argument defaults +] + +[tool.ruff.lint.isort] +known-first-party = ["[[ project_name ]]"] diff --git a/src/djinit/templates/project/readme.md-tpl b/src/djinit/templates/project/readme.md-tpl new file mode 100644 index 0000000..9c28632 --- /dev/null +++ b/src/djinit/templates/project/readme.md-tpl @@ -0,0 +1,201 @@ +# [[ project_name ]] + +A Django project folder structure is generated 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 +- `just install ` - Install package +- `just remove ` - Remove package +- `just server` - Run production server +- `just ci` - Run all linting and checks +- `just test-coverage` - Run tests with coverage + +## 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 +├── uv.lock +├── .env.sample +├── justfile +├── Procfile +├── runtime.txt +└── README.md +# @ELSEIF 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 +├── uv.lock +├── .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 +├── uv.lock +├── .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/ diff --git a/src/djinit/templates/project/requirements-tpl b/src/djinit/templates/project/requirements-tpl new file mode 100644 index 0000000..cb48ff9 --- /dev/null +++ b/src/djinit/templates/project/requirements-tpl @@ -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 +dj-database-url # @IF use_database_url diff --git a/src/djinit/templates/project/runtime_txt-tpl b/src/djinit/templates/project/runtime_txt-tpl new file mode 100644 index 0000000..34ffefa --- /dev/null +++ b/src/djinit/templates/project/runtime_txt-tpl @@ -0,0 +1 @@ +python-[[ python_version ]] diff --git a/src/djinit/ui/__init__.py b/src/djinit/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/djinit/ui/console.py b/src/djinit/ui/console.py new file mode 100644 index 0000000..a9f07c4 --- /dev/null +++ b/src/djinit/ui/console.py @@ -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() diff --git a/src/djinit/ui/input.py b/src/djinit/ui/input.py new file mode 100644 index 0000000..7482a28 --- /dev/null +++ b/src/djinit/ui/input.py @@ -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 CommonUtils +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 = CommonUtils.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 = CommonUtils.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" diff --git a/src/djinit/utils/__init__.py b/src/djinit/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/djinit/utils/common.py b/src/djinit/utils/common.py new file mode 100644 index 0000000..ebd3f92 --- /dev/null +++ b/src/djinit/utils/common.py @@ -0,0 +1,340 @@ +""" +Shared utility functions. +Contains common operations used across multiple managers. +""" + +import os +import subprocess +import sys +from contextlib import contextmanager + +from djinit.core.base import BaseUtils +from djinit.templater import template_engine +from djinit.ui.console import UIFormatter + + +class CommonUtils(BaseUtils): + @staticmethod + def format_file(filename: str) -> None: + """Format Python file using Ruff formatter.""" + subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True) + + @staticmethod + def create_file_with_content( + filename: str, content: str, success_message: str, should_format: bool = False + ) -> None: + from djinit.utils.exceptions import FileError + + try: + with open(filename, "w") as file: + file.write(content) + except OSError as e: + raise FileError(f"Failed to write file: {filename}", details=str(e)) from e + + if should_format: + CommonUtils.format_file(filename) + + UIFormatter.print_success(success_message) + + @staticmethod + def create_file_from_template( + file_path: str, + template_path: str, + context: dict, + success_message: str, + should_format: bool = True, + ) -> None: + try: + content = template_engine.render_template(template_path, context) + except Exception as e: + from djinit.utils.exceptions import TemplateError + + raise TemplateError(f"Failed to render template: {template_path}", details=str(e)) from e + + CommonUtils.create_file_with_content(file_path, content, success_message, should_format=should_format) + + @staticmethod + def create_init_file(directory: str, success_message: str) -> None: + init_path = os.path.join(directory, "__init__.py") + CommonUtils.create_file_from_template( + init_path, "project/init.py-tpl", {}, success_message, should_format=False + ) + + @staticmethod + def create_directory_with_init(dir_path: str, message: str) -> None: + """Create directory and __init__.py file.""" + os.makedirs(dir_path, exist_ok=True) + CommonUtils.create_init_file(dir_path, message) + + @staticmethod + def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None: + """Create multiple files from templates. + + Args: + base_dir: Base directory where files will be created + files: List of tuples (filename, template_path, context) + prefix: Optional prefix for success message + should_format: Whether to format the created files + """ + for file_spec in files: + if len(file_spec) == 3: + filename, template_path, context = file_spec + else: + # Support (filename, template_path) format with empty context + filename, template_path = file_spec + context = {} + filepath = os.path.join(base_dir, filename) + message = f"Created {prefix}{filename}" if prefix else f"Created {filename}" + CommonUtils.create_file_from_template( + filepath, template_path, context, message, should_format=should_format + ) + + @staticmethod + def get_package_name(project_dir: str) -> str: + """Get package name, defaulting to 'backend' if project_dir is '.' or empty.""" + return "backend" if project_dir == "." or not project_dir else project_dir + + @staticmethod + @contextmanager + def change_cwd(path: str): + original_cwd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(original_cwd) + + @staticmethod + def _parse_app_entry_from_line(line: str) -> str | None: + """Extract app name from a line in USER_DEFINED_APPS section.""" + line_stripped = line.strip().strip(",").strip() + for quote in ('"', "'"): + if line_stripped.startswith(quote) and line_stripped.endswith(quote): + return line_stripped.strip(quote) + return None + + @staticmethod + def _is_user_apps_section_start(line: str) -> bool: + """Check if line starts USER_DEFINED_APPS section.""" + return "USER_DEFINED_APPS" in line and "=" in line + + @staticmethod + def _iterate_user_apps_lines(content: str): + """Generator that yields lines within USER_DEFINED_APPS section.""" + lines = content.split("\n") + in_user_apps = False + + for line in lines: + if CommonUtils._is_user_apps_section_start(line): + in_user_apps = True + elif in_user_apps and line.strip() == "]": + break + elif in_user_apps: + yield line + + @staticmethod + def extract_existing_apps(content: str) -> set: + existing_apps = set() + for line in CommonUtils._iterate_user_apps_lines(content): + app_name = CommonUtils._parse_app_entry_from_line(line) + if app_name: + existing_apps.add(app_name) + return existing_apps + + @staticmethod + def format_app_entries(apps: list) -> list: + return [f' "{app}",' for app in apps] + + @staticmethod + def is_app_section_start(line: str) -> bool: + return "=" in line and any( + keyword in line for keyword in ("BUILT_IN_APPS", "THIRD_PARTY_APPS", "INSTALLED_APPS") + ) + + @staticmethod + def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool: + for next_idx in range(current_idx + 1, min(current_idx + 3, len(lines))): + next_line = lines[next_idx].strip() + if next_line and "BUILT_IN_APPS" in next_line: + return True + return True # Default to True if we're inside USER_DEFINED_APPS + + @staticmethod + def split_bracket_line(line: str, apps_to_add: list) -> list: + bracket_pos = line.rfind("]") + before_bracket = line[:bracket_pos].rstrip() if bracket_pos > 0 else line.rstrip() + after_bracket = line[bracket_pos:] if bracket_pos > 0 else "]" + + result = [before_bracket] + result.extend(CommonUtils.format_app_entries(apps_to_add)) + result.append(after_bracket) + return result + + @staticmethod + def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str: + lines = content.split("\n") + new_lines = [] + in_user_apps = False + apps_added = False + + for i, line in enumerate(lines): + if CommonUtils._is_user_apps_section_start(line) and not apps_added: + in_user_apps = True + + if "]" in line: + new_lines.extend(CommonUtils.split_bracket_line(line, apps_to_add)) + apps_added = True + in_user_apps = False + else: + new_lines.append(line) + + elif in_user_apps and line.strip() == "]" and not apps_added: + if CommonUtils.is_user_apps_closing_bracket(lines, i): + new_lines.extend(CommonUtils.format_app_entries(apps_to_add)) + new_lines.append(line) + apps_added = True + in_user_apps = False + else: + new_lines.append(line) + + elif in_user_apps and CommonUtils.is_app_section_start(line) and not apps_added: + new_lines.extend(CommonUtils.format_app_entries(apps_to_add)) + new_lines.append("]") + new_lines.append(line) + apps_added = True + in_user_apps = False + + else: + new_lines.append(line) + + if not apps_added: + UIFormatter.print_error("Could not find or update USER_DEFINED_APPS section in base.py") + return None + + return "\n".join(new_lines) + + @staticmethod + def replace_app_in_user_defined_apps(content: str, old_app: str, new_app: str) -> str | None: + """Replace an app entry in USER_DEFINED_APPS with a new one.""" + lines = content.split("\n") + new_lines = [] + replaced = False + + for line in lines: + if not replaced and CommonUtils._parse_app_entry_from_line(line) == old_app: + # Try to preserve indentation + indent_idx = 0 + for char in line: + if char.isspace(): + indent_idx += 1 + else: + break + indent = line[:indent_idx] if indent_idx > 0 else " " + + new_lines.append(f'{indent}"{new_app}",') + replaced = True + else: + new_lines.append(line) + + if replaced: + return "\n".join(new_lines) + return None + + @staticmethod + def calculate_app_module_paths(app_names: list, metadata: dict) -> list: + nested = bool(metadata.get("nested_apps")) + nested_dir_name = metadata.get("nested_dir") if nested else None + return [CommonUtils.calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names] + + @staticmethod + def calculate_app_module_path(app_name: str, nested: bool, nested_dir: str | None) -> str: + return f"{nested_dir}.{app_name}" if nested and nested_dir else app_name + + @staticmethod + def is_django_project(directory: str = None) -> bool: + if directory is None: + directory = os.getcwd() + manage_py_path = os.path.join(directory, "manage.py") + return os.path.exists(manage_py_path) + + @staticmethod + def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]: + if search_dir is None: + search_dir = os.getcwd() + + for item in os.listdir(search_dir): + if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__": + candidate = os.path.join(search_dir, item) + base_py = os.path.join(candidate, "settings", "base.py") + if os.path.exists(base_py): + return candidate, base_py + return None, None + + @staticmethod + def find_settings_path(search_dir: str = None) -> str | None: + project_dir, settings_base_path = CommonUtils.find_project_dir(search_dir) + if settings_base_path: + return os.path.dirname(settings_base_path) + return None + + @staticmethod + def get_base_settings_path(project_root: str, project_name: str) -> str: + settings_dir = os.path.join(project_root, project_name, "settings") + return os.path.join(settings_dir, "base.py") + + @staticmethod + def read_base_settings(project_root: str, project_name: str) -> str | None: + base_settings_path = CommonUtils.get_base_settings_path(project_root, project_name) + if not os.path.exists(base_settings_path): + return None + + with open(base_settings_path) as f: + return f.read() + + @staticmethod + def detect_nested_structure_from_settings( + base_settings_path: str, search_dir: str = None + ) -> tuple[bool, str | None, str]: + if search_dir is None: + search_dir = os.getcwd() + + if not os.path.exists(base_settings_path): + return False, None, search_dir + + try: + with open(base_settings_path) as f: + base_content = f.read() + + for line in CommonUtils._iterate_user_apps_lines(base_content): + app_label = CommonUtils._parse_app_entry_from_line(line) + if app_label and "." in app_label: + nested_dir_name = app_label.split(".", 1)[0] + nested_path = os.path.join(search_dir, nested_dir_name) + if os.path.isdir(nested_path): + return True, nested_dir_name, nested_path + except Exception: + pass + + return False, None, search_dir + + @staticmethod + def get_djinit_config(project_root: str = None) -> dict | None: + """Read .djinit configuration file.""" + import json + + if project_root is None: + project_root = os.getcwd() + + config_path = os.path.join(project_root, ".djinit") + if not os.path.exists(config_path): + # Try finding it in parent directories + parent = os.path.dirname(project_root) + if parent and parent != project_root: + return CommonUtils.get_djinit_config(parent) + return None + + try: + with open(config_path) as f: + return json.load(f) + except Exception: + return None diff --git a/src/djinit/utils/django.py b/src/djinit/utils/django.py new file mode 100644 index 0000000..1f74d6e --- /dev/null +++ b/src/djinit/utils/django.py @@ -0,0 +1,116 @@ +""" +Django helper module that replicates Django's startproject and startapp functionality. + +This module provides functions to create Django projects and apps without requiring Django. +""" + +import os + +from djinit.core.config import DJANGO_VERSION +from djinit.utils.common import CommonUtils + + +class DjangoHelper: + DJANGO_VERSION = DJANGO_VERSION + + @staticmethod + def startproject(project_name: str, directory: str, unified: bool = False) -> None: + try: + os.makedirs(directory, exist_ok=True) + + manage_py_path = os.path.join(directory, "manage.py") + CommonUtils.create_file_from_template( + manage_py_path, "project/manage.py-tpl", {"project_name": project_name}, "Created manage.py" + ) + os.chmod(manage_py_path, 0o755) + + if unified: + return + + project_config_dir = os.path.join(directory, project_name) + CommonUtils.create_directory_with_init(project_config_dir, f"Created {project_name}/__init__.py") + + settings_dir = os.path.join(project_config_dir, "settings") + CommonUtils.create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py") + + base_context = {"project_name": project_name, "app_names": []} + settings_files = [ + ("base.py", "config/settings/base.py-tpl", base_context), + ("development.py", "config/settings/development.py-tpl", {}), + ("production.py", "config/settings/production.py-tpl", {}), + ] + CommonUtils.create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/") + + urls_context = { + "url_type": "project", + "project_name": project_name, + "django_version": DjangoHelper.DJANGO_VERSION, + } + project_files = [ + ("urls.py", "config/urls.py-tpl", urls_context), + ("wsgi.py", "config/wsgi.py-tpl", {}), + ("asgi.py", "config/asgi.py-tpl", {}), + ] + CommonUtils.create_files_from_templates(project_config_dir, project_files, f"{project_name}/") + + except Exception as e: + from djinit.utils.exceptions import DjinitError + + raise DjinitError(f"Error creating Django project: {e}", details=str(e)) from e + + @staticmethod + def startapp(app_name: str, directory: str, app_module: str = None) -> bool: + """Create a Django app with standard structure (empty files, no example code). + + Args: + app_name: Name of the app to create + directory: Directory where the app should be created + app_module: Full module path (e.g., 'apps.users' for nested apps) + + Returns: + bool: True if successful, False otherwise + """ + try: + app_dir = os.path.join(directory, app_name) + os.makedirs(app_dir, exist_ok=True) + + # Prepare context + app_config_name = app_name.title().replace("_", "") + context = { + "app_name": app_name, + "app_config_name": app_config_name, + "django_version": DjangoHelper.DJANGO_VERSION, + } + + # Use provided app_module or default to app_name (path calculation handled by caller) + if not app_module: + app_module = app_name + + context["app_module"] = app_module + + # Create __init__.py + CommonUtils.create_init_file(app_dir, f"Created {app_name}/__init__.py") + + # Create all standard Django app files (empty templates with basic structure) + app_files = [ + ("apps.py", "components/apps.py-tpl", context), + ("models.py", "components/models.py-tpl", context), + ("views.py", "components/views.py-tpl", context), + ("admin.py", "components/admin.py-tpl", context), + ("serializers.py", "components/serializers.py-tpl", context), + ("urls.py", "components/urls.py-tpl", context), + ("tests.py", "components/tests.py-tpl", context), + ] + + CommonUtils.create_files_from_templates(app_dir, app_files, f"{app_name}/") + + # Create migrations directory + migrations_dir = os.path.join(app_dir, "migrations") + CommonUtils.create_directory_with_init(migrations_dir, f"Created {app_name}/migrations/__init__.py") + + return True + + except Exception as e: + from djinit.utils.exceptions import DjinitError + + raise DjinitError(f"Error creating Django app: {e}", details=str(e)) from e diff --git a/src/djinit/utils/exceptions.py b/src/djinit/utils/exceptions.py new file mode 100644 index 0000000..f8e8b65 --- /dev/null +++ b/src/djinit/utils/exceptions.py @@ -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 diff --git a/src/djinit/utils/secretkey.py b/src/djinit/utils/secretkey.py new file mode 100644 index 0000000..67daba5 --- /dev/null +++ b/src/djinit/utils/secretkey.py @@ -0,0 +1,39 @@ +""" +Django Secret Key Generator + +Generate secure Django secret keys for different environments. +""" + +import secrets +import string + +from rich.console import Console +from rich.table import Table + +console = Console() + + +def generate_secret_key(length: int = 50) -> str: + alphabet = string.ascii_letters + string.digits + "!@#$%^&*(-_=+)" + return "".join(secrets.choice(alphabet) for _ in range(length)) + + +def generate_multiple_keys(count: int = 3, length: int = 50) -> list[str]: + return [generate_secret_key(length) for _ in range(count)] + + +def display_secret_keys(keys: list[str]) -> None: + table = Table(title="🔐 Django Secret Keys", show_header=True, header_style="bold blue") + table.add_column("Environment", style="cyan", no_wrap=True) + table.add_column("Secret Key", style="dim") + + for i, key in enumerate(keys, 1): + table.add_row(f"Environment {i}", key) + + console.print(table) + console.print() + + +if __name__ == "__main__": + keys = generate_multiple_keys(3, 50) + display_secret_keys(keys) diff --git a/src/djinit/utils/validators.py b/src/djinit/utils/validators.py new file mode 100644 index 0000000..4562a0a --- /dev/null +++ b/src/djinit/utils/validators.py @@ -0,0 +1,47 @@ +""" +Input validation utilities for Django project setup. +Handles validation of project names, app names, and other user inputs. +""" + +import keyword +import re +from typing import Any, Callable, Tuple + +from djinit.core.config import PYTHON_BUILTINS + +NAME_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*$") + + +def _validate_name(name: str, name_type: str = "name") -> Tuple[bool, str]: + validation_rules: list[Tuple[Callable[[str], Any], str]] = [ + (lambda n: bool(n and n.strip()), f"{name_type.capitalize()} cannot be empty"), + (lambda n: len(n.strip()) >= 2, f"{name_type.capitalize()} must be at least 2 characters long"), + (lambda n: len(n.strip()) <= 50, f"{name_type.capitalize()} must be less than 50 characters"), + ( + lambda n: NAME_PATTERN.match(n.strip()), + f"{name_type.capitalize()} must start with a letter and contain only letters, numbers, and underscores", + ), + ( + lambda n: not keyword.iskeyword(n.strip()), + f"'{name.strip()}' is a Python keyword. Please choose a different name", + ), + ( + lambda n: n.strip().lower() not in PYTHON_BUILTINS, + f"'{name.strip()}' conflicts with Python builtin module. Choose a different name", + ), + (lambda n: not n.strip().startswith("_"), f"{name_type.capitalize()} should not start with underscore"), + ] + + for rule, error_message in validation_rules: + if not rule(name): + return False, error_message + + return True, "" + + +def validate_project_name(name: str) -> Tuple[bool, str]: + return _validate_name(name, "project name") + + +def validate_app_name(name: str) -> Tuple[bool, str]: + return _validate_name(name, "app name") -- 2.43.0 From e24fb36dead22ff0aea76d88571314e337069aad Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 22:23:57 +0545 Subject: [PATCH 20/24] chore(djinit/templates): purge obsolete template components Remove legacy template placeholders that are no longer used in the project scaffold. - New Features: - Enhancements: Removed unused template files - Bug Fixes: --- djinit/templates/components/admin.py-tpl | 5 - djinit/templates/components/api_readme.md-tpl | 76 ------ djinit/templates/components/apps.py-tpl | 12 - djinit/templates/components/models.py-tpl | 3 - .../templates/components/models_readme.md-tpl | 119 ---------- djinit/templates/components/routes.py-tpl | 8 - .../templates/components/serializers.py-tpl | 3 - djinit/templates/components/tests.py-tpl | 3 - djinit/templates/components/views.py-tpl | 3 - djinit/templates/config/asgi.py-tpl | 11 - djinit/templates/config/settings/base.py-tpl | 179 -------------- .../config/settings/development.py-tpl | 42 ---- .../config/settings/production.py-tpl | 65 ------ djinit/templates/config/urls.py-tpl | 103 -------- djinit/templates/config/wsgi.py-tpl | 12 - .../predefined/apps/generic/models.py-tpl | 18 -- .../apps/generic/serializers.py-tpl | 11 - .../predefined/apps/generic/services.py-tpl | 36 --- .../predefined/apps/generic/tests.py-tpl | 55 ----- .../predefined/apps/generic/views.py-tpl | 62 ----- .../predefined/apps/users/models.py-tpl | 9 - .../predefined/apps/users/serializers.py-tpl | 9 - .../predefined/apps/users/services.py-tpl | 10 - .../predefined/apps/users/tests.py-tpl | 12 - .../predefined/apps/users/views.py-tpl | 13 -- .../presets/predefined/core/exceptions.py-tpl | 5 - .../core/middleware/request_logger.py-tpl | 14 -- .../core/mixins/timestamped_model.py-tpl | 9 - .../predefined/core/utils/responses.py-tpl | 17 -- .../presets/unified/apps/core/apps.py-tpl | 8 - .../unified/apps/core/models/__init__.py-tpl | 4 - .../unified/apps/core/models/base.py-tpl | 13 -- .../unified/apps/core/models/mixins.py-tpl | 13 -- .../unified/apps/core/utils/__init__.py-tpl | 3 - .../templates/project/ci/github_actions-tpl | 109 --------- djinit/templates/project/ci/gitlab_ci-tpl | 133 ----------- djinit/templates/project/env_sample-tpl | 32 --- djinit/templates/project/gitignore-tpl | 93 -------- djinit/templates/project/init.py-tpl | 0 djinit/templates/project/justfile-tpl | 219 ------------------ djinit/templates/project/manage.py-tpl | 32 --- djinit/templates/project/procfile-tpl | 20 -- djinit/templates/project/pyproject.toml-tpl | 46 ---- djinit/templates/project/readme.md-tpl | 201 ---------------- djinit/templates/project/requirements-tpl | 19 -- djinit/templates/project/runtime_txt-tpl | 1 - 46 files changed, 1870 deletions(-) delete mode 100644 djinit/templates/components/admin.py-tpl delete mode 100644 djinit/templates/components/api_readme.md-tpl delete mode 100644 djinit/templates/components/apps.py-tpl delete mode 100644 djinit/templates/components/models.py-tpl delete mode 100644 djinit/templates/components/models_readme.md-tpl delete mode 100644 djinit/templates/components/routes.py-tpl delete mode 100644 djinit/templates/components/serializers.py-tpl delete mode 100644 djinit/templates/components/tests.py-tpl delete mode 100644 djinit/templates/components/views.py-tpl delete mode 100644 djinit/templates/config/asgi.py-tpl delete mode 100644 djinit/templates/config/settings/base.py-tpl delete mode 100644 djinit/templates/config/settings/development.py-tpl delete mode 100644 djinit/templates/config/settings/production.py-tpl delete mode 100644 djinit/templates/config/urls.py-tpl delete mode 100644 djinit/templates/config/wsgi.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/generic/models.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/generic/serializers.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/generic/services.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/generic/tests.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/generic/views.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/users/models.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/users/serializers.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/users/services.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/users/tests.py-tpl delete mode 100644 djinit/templates/presets/predefined/apps/users/views.py-tpl delete mode 100644 djinit/templates/presets/predefined/core/exceptions.py-tpl delete mode 100644 djinit/templates/presets/predefined/core/middleware/request_logger.py-tpl delete mode 100644 djinit/templates/presets/predefined/core/mixins/timestamped_model.py-tpl delete mode 100644 djinit/templates/presets/predefined/core/utils/responses.py-tpl delete mode 100644 djinit/templates/presets/unified/apps/core/apps.py-tpl delete mode 100644 djinit/templates/presets/unified/apps/core/models/__init__.py-tpl delete mode 100644 djinit/templates/presets/unified/apps/core/models/base.py-tpl delete mode 100644 djinit/templates/presets/unified/apps/core/models/mixins.py-tpl delete mode 100644 djinit/templates/presets/unified/apps/core/utils/__init__.py-tpl delete mode 100644 djinit/templates/project/ci/github_actions-tpl delete mode 100644 djinit/templates/project/ci/gitlab_ci-tpl delete mode 100644 djinit/templates/project/env_sample-tpl delete mode 100644 djinit/templates/project/gitignore-tpl delete mode 100644 djinit/templates/project/init.py-tpl delete mode 100644 djinit/templates/project/justfile-tpl delete mode 100644 djinit/templates/project/manage.py-tpl delete mode 100644 djinit/templates/project/procfile-tpl delete mode 100644 djinit/templates/project/pyproject.toml-tpl delete mode 100644 djinit/templates/project/readme.md-tpl delete mode 100644 djinit/templates/project/requirements-tpl delete mode 100644 djinit/templates/project/runtime_txt-tpl diff --git a/djinit/templates/components/admin.py-tpl b/djinit/templates/components/admin.py-tpl deleted file mode 100644 index dc9bfc6..0000000 --- a/djinit/templates/components/admin.py-tpl +++ /dev/null @@ -1,5 +0,0 @@ -"""generated with djinit""" - -from django.contrib import admin - -# Register your models here. diff --git a/djinit/templates/components/api_readme.md-tpl b/djinit/templates/components/api_readme.md-tpl deleted file mode 100644 index 5d416ff..0000000 --- a/djinit/templates/components/api_readme.md-tpl +++ /dev/null @@ -1,76 +0,0 @@ -# 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 -└── / # 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 diff --git a/djinit/templates/components/apps.py-tpl b/djinit/templates/components/apps.py-tpl deleted file mode 100644 index fdac009..0000000 --- a/djinit/templates/components/apps.py-tpl +++ /dev/null @@ -1,12 +0,0 @@ -from django.apps import AppConfig - - - -class [[ app_config_name ]]Config(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' -# @IF app_module - name = '[[ app_module ]]' -# @ENDIF -# @IF not app_module - name = '[[ app_name ]]' -# @ENDIF diff --git a/djinit/templates/components/models.py-tpl b/djinit/templates/components/models.py-tpl deleted file mode 100644 index 71a8362..0000000 --- a/djinit/templates/components/models.py-tpl +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/djinit/templates/components/models_readme.md-tpl b/djinit/templates/components/models_readme.md-tpl deleted file mode 100644 index 0bf247c..0000000 --- a/djinit/templates/components/models_readme.md-tpl +++ /dev/null @@ -1,119 +0,0 @@ -# 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() -``` diff --git a/djinit/templates/components/routes.py-tpl b/djinit/templates/components/routes.py-tpl deleted file mode 100644 index 37b00a9..0000000 --- a/djinit/templates/components/routes.py-tpl +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework.routers import DefaultRouter - -router = DefaultRouter() - -# Register your viewsets here -# router.register(r'endpoint', YourViewSet, basename='your-endpoint') - -urlpatterns = router.urls diff --git a/djinit/templates/components/serializers.py-tpl b/djinit/templates/components/serializers.py-tpl deleted file mode 100644 index 2774c36..0000000 --- a/djinit/templates/components/serializers.py-tpl +++ /dev/null @@ -1,3 +0,0 @@ -from rest_framework import serializers - -# Create your serializers here. diff --git a/djinit/templates/components/tests.py-tpl b/djinit/templates/components/tests.py-tpl deleted file mode 100644 index 7ce503c..0000000 --- a/djinit/templates/components/tests.py-tpl +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/djinit/templates/components/views.py-tpl b/djinit/templates/components/views.py-tpl deleted file mode 100644 index 91ea44a..0000000 --- a/djinit/templates/components/views.py-tpl +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/djinit/templates/config/asgi.py-tpl b/djinit/templates/config/asgi.py-tpl deleted file mode 100644 index 0c5c879..0000000 --- a/djinit/templates/config/asgi.py-tpl +++ /dev/null @@ -1,11 +0,0 @@ -"""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="[[ project_name ]].settings.production")) -application = get_asgi_application() diff --git a/djinit/templates/config/settings/base.py-tpl b/djinit/templates/config/settings/base.py-tpl deleted file mode 100644 index f33b399..0000000 --- a/djinit/templates/config/settings/base.py-tpl +++ /dev/null @@ -1,179 +0,0 @@ -""" -generated with djinit -Common settings shared between development and production environment -""" - -import os -from datetime import timedelta -from pathlib import Path - -import environ - -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 - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ - - -# Application definition -THIRD_PARTY_APPS = [ - "rest_framework", - "rest_framework_simplejwt", - "rest_framework_simplejwt.token_blacklist", - "corsheaders", - "drf_spectacular", -] - -USER_DEFINED_APPS = [ -# @LOOP app in app_names "[[ app ]]", -# @ENDLOOP -] - -BUILT_IN_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", -] - -INSTALLED_APPS = BUILT_IN_APPS + THIRD_PARTY_APPS + USER_DEFINED_APPS -INSTALLED_APPS.insert(0, "jazzmin") - -MIDDLEWARE = [ - "corsheaders.middleware.CorsMiddleware", - "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -ROOT_URLCONF = "[[ project_name ]].urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -WSGI_APPLICATION = "[[ project_name ]].wsgi.application" - - -# Password validation -# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/5.2/topics/i18n/ - -LANGUAGE_CODE = "en-us" -TIME_ZONE = "UTC" -USE_I18N = True -USE_TZ = True - -# Static files and media files -STATIC_URL = "static/" -STATIC_ROOT = BASE_DIR / "staticfiles" - -MEDIA_URL = "media/" -MEDIA_ROOT = BASE_DIR / "media" - -STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - -# REST Framework Settings -REST_FRAMEWORK = { - "DEFAULT_RENDERER_CLASSES": [ - "rest_framework.renderers.JSONRenderer", - ], - "DEFAULT_PARSER_CLASSES": [ - "rest_framework.parsers.JSONParser", - ], - "DEFAULT_AUTHENTICATION_CLASSES": ( - "rest_framework_simplejwt.authentication.JWTAuthentication", - "rest_framework.authentication.SessionAuthentication", - ), - "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", - "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", - "PAGE_SIZE": 20, -} - -# DRF Spectacular settings -SPECTACULAR_SETTINGS = { - "TITLE": "[[ project_name ]] API", - "DESCRIPTION": "API documentation for [[ project_name ]]", - "VERSION": "1.0.0", - "SERVE_INCLUDE_SCHEMA": False, - "COMPONENT_SPLIT_REQUEST": True, - "SCHEMA_PATH_PREFIX": "/api/", -} - -# JWT Settings -SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(days=1), - "REFRESH_TOKEN_LIFETIME": timedelta(days=3), - "ROTATE_REFRESH_TOKENS": True, - "BLACKLIST_AFTER_ROTATION": True, -} - -# CORS Settings -CORS_ALLOW_METHODS = [ - "DELETE", - "GET", - "OPTIONS", - "PATCH", - "POST", - "PUT", -] - -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 -X_FRAME_OPTIONS = "DENY" diff --git a/djinit/templates/config/settings/development.py-tpl b/djinit/templates/config/settings/development.py-tpl deleted file mode 100644 index 2e3c1b3..0000000 --- a/djinit/templates/config/settings/development.py-tpl +++ /dev/null @@ -1,42 +0,0 @@ -"""generated with djinit""" - -from .base import * - -SECRET_KEY = "[[ secret_key ]]" - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = ["*"] - -# Database -# https://docs.djangoproject.com/en/5.2/ref/settings/#databases - -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; - """, - }, - } -} - -# Email backend for development -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 diff --git a/djinit/templates/config/settings/production.py-tpl b/djinit/templates/config/settings/production.py-tpl deleted file mode 100644 index 7d58363..0000000 --- a/djinit/templates/config/settings/production.py-tpl +++ /dev/null @@ -1,65 +0,0 @@ -"""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' diff --git a/djinit/templates/config/urls.py-tpl b/djinit/templates/config/urls.py-tpl deleted file mode 100644 index 574294f..0000000 --- a/djinit/templates/config/urls.py-tpl +++ /dev/null @@ -1,103 +0,0 @@ -# @IF url_type == 'project' -"""generated with djinit""" - -from django.contrib import admin -from django.urls import path -# @IF include_api or include_apps -from django.urls import include -# @ENDIF -# @IF include_api -from django.conf import settings -from drf_spectacular.views import ( - SpectacularAPIView, - SpectacularSwaggerView, -) -from rest_framework_simplejwt.views import ( - TokenObtainPairView, - TokenRefreshView, - TokenBlacklistView, -) -# @ENDIF - -urlpatterns = [ - path("admin/", admin.site.urls), -# @IF include_api - # JWT tokens - path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), - path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), - path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"), -# @ENDIF -# @IF include_api and comment_out_api_url - # path("api/", include("[[ api_module ]].urls")), -# @ENDIF -# @IF include_api and not comment_out_api_url - path("api/", include("[[ api_module ]].urls")), -# @ENDIF -# @IF include_apps - # App URLs -# @LOOP app in app_names - path("", include("[[ app ]].urls")), -# @ENDLOOP -# @ENDIF -] -# @IF include_api - -# Don't show schema in production -if settings.DEBUG: - urlpatterns += [ - path("schema/", SpectacularAPIView.as_view(), name="schema"), - path("docs/", SpectacularSwaggerView.as_view(url_name="schema")), - ] -# @ENDIF -# @ENDIF -# @IF url_type == 'api_root' -from django.urls import include, path - -app_name = "[[ app_name ]]" - -urlpatterns = [ - path("[[ version ]]/", include("[[ api_module ]].[[ version ]].urls")), -] -# @ENDIF -# @IF url_type == 'api_version' -from django.urls import include, path - -app_name = "[[ app_name ]]" - -urlpatterns = [ -# @LOOP app in app_list - path("[[ app ]]/", include("[[ app_module ]].[[ app ]].urls")), -# @ENDLOOP -] -# @ENDIF -# @IF url_type == 'drf_router' -from rest_framework.routers import DefaultRouter -# @IF has_viewset -from [[ app_module ]].views.[[ view_file ]] import [[ viewset_name ]] -# @ENDIF -# @IF has_app_name - -app_name = "[[ app_name ]]" -# @ENDIF - -router = DefaultRouter() -# @IF has_viewset -router.register(r"[[ router_prefix ]]", [[ viewset_name ]], basename="[[ basename ]]") -# @ENDIF - -urlpatterns = [ -# @IF has_include - path("", include(router.urls)), -# @ENDIF -] -# @IF not has_include -urlpatterns = router.urls -# @ENDIF -# @ENDIF -# @IF url_type == 'component' -from django.urls import path - -urlpatterns = [ - # Add your URL patterns here -] -# @ENDIF diff --git a/djinit/templates/config/wsgi.py-tpl b/djinit/templates/config/wsgi.py-tpl deleted file mode 100644 index 421456d..0000000 --- a/djinit/templates/config/wsgi.py-tpl +++ /dev/null @@ -1,12 +0,0 @@ -"""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="[[ project_name ]].settings.production")) - -application = get_wsgi_application() diff --git a/djinit/templates/presets/predefined/apps/generic/models.py-tpl b/djinit/templates/presets/predefined/apps/generic/models.py-tpl deleted file mode 100644 index bbae956..0000000 --- a/djinit/templates/presets/predefined/apps/generic/models.py-tpl +++ /dev/null @@ -1,18 +0,0 @@ -from django.db import models - - -class [[ model_class_name ]](models.Model): - """[[ model_class_name ]] model.""" - - # Add your fields here - name = models.CharField(max_length=255) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - verbose_name = "[[ model_class_name ]]" - verbose_name_plural = "[[ model_class_name ]]s" - ordering = ["-created_at"] - - def __str__(self): - return self.name diff --git a/djinit/templates/presets/predefined/apps/generic/serializers.py-tpl b/djinit/templates/presets/predefined/apps/generic/serializers.py-tpl deleted file mode 100644 index 30b7189..0000000 --- a/djinit/templates/presets/predefined/apps/generic/serializers.py-tpl +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework import serializers -from [[ app_module ]].models.[[ app_name ]] import [[ model_class_name ]] - - -class [[ model_class_name ]]Serializer(serializers.ModelSerializer): - """Serializer for [[ model_class_name ]] model.""" - - class Meta: - model = [[ model_class_name ]] - fields = "__all__" - read_only_fields = ["id", "created_at", "updated_at"] diff --git a/djinit/templates/presets/predefined/apps/generic/services.py-tpl b/djinit/templates/presets/predefined/apps/generic/services.py-tpl deleted file mode 100644 index de45859..0000000 --- a/djinit/templates/presets/predefined/apps/generic/services.py-tpl +++ /dev/null @@ -1,36 +0,0 @@ -from [[ app_module ]].models.[[ app_name ]] import [[ model_class_name ]] - - -class [[ model_class_name ]]Service: - """Service class for [[ model_class_name ]] business logic.""" - - @staticmethod - def get_all(): - """Get all [[ model_class_name ]] instances.""" - return [[ model_class_name ]].objects.all() - - @staticmethod - def get_by_id(id): - """Get [[ model_class_name ]] by ID.""" - try: - return [[ model_class_name ]].objects.get(id=id) - except [[ model_class_name ]].DoesNotExist: - return None - - @staticmethod - def create(data): - """Create a new [[ model_class_name ]] instance.""" - return [[ model_class_name ]].objects.create(**data) - - @staticmethod - def update(instance, data): - """Update an existing [[ model_class_name ]] instance.""" - for key, value in data.items(): - setattr(instance, key, value) - instance.save() - return instance - - @staticmethod - def delete(instance): - """Delete a [[ model_class_name ]] instance.""" - instance.delete() diff --git a/djinit/templates/presets/predefined/apps/generic/tests.py-tpl b/djinit/templates/presets/predefined/apps/generic/tests.py-tpl deleted file mode 100644 index 8dc7a7e..0000000 --- a/djinit/templates/presets/predefined/apps/generic/tests.py-tpl +++ /dev/null @@ -1,55 +0,0 @@ -from rest_framework.test import APITestCase -from rest_framework import status -from django.contrib.auth import get_user_model - -from [[ app_module ]].models.[[ app_name ]] import [[ model_class_name ]] - -User = get_user_model() - - -class [[ model_class_name ]]APITestCase(APITestCase): - """Test cases for [[ model_class_name ]] API endpoints.""" - - def setUp(self): - """Set up test data.""" - self.user = User.objects.create_user( - username="testuser", password="testpass123" - ) - self.client.force_authenticate(user=self.user) - self.test_data = {"name": "Test [[ model_class_name ]]"} - - def test_create_[[ app_name ]](self): - """Test creating a [[ model_class_name ]].""" - response = self.client.post("/api/v1/[[ app_name ]]/", self.test_data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual([[ model_class_name ]].objects.count(), 1) - - def test_list_[[ app_name ]]s(self): - """Test listing all [[ model_class_name ]]s.""" - [[ model_class_name ]].objects.create(**self.test_data) - response = self.client.get("/api/v1/[[ app_name ]]/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - - def test_retrieve_[[ app_name ]](self): - """Test retrieving a single [[ model_class_name ]].""" - instance = [[ model_class_name ]].objects.create(**self.test_data) - response = self.client.get(f"/api/v1/[[ app_name ]]/{instance.id}/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["name"], self.test_data["name"]) - - def test_update_[[ app_name ]](self): - """Test updating a [[ model_class_name ]].""" - instance = [[ model_class_name ]].objects.create(**self.test_data) - updated_data = {"name": "Updated [[ model_class_name ]]"} - response = self.client.patch(f"/api/v1/[[ app_name ]]/{instance.id}/", updated_data) - self.assertEqual(response.status_code, status.HTTP_200_OK) - instance.refresh_from_db() - self.assertEqual(instance.name, updated_data["name"]) - - def test_delete_[[ app_name ]](self): - """Test deleting a [[ model_class_name ]].""" - instance = [[ model_class_name ]].objects.create(**self.test_data) - response = self.client.delete(f"/api/v1/[[ app_name ]]/{instance.id}/") - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual([[ model_class_name ]].objects.count(), 0) diff --git a/djinit/templates/presets/predefined/apps/generic/views.py-tpl b/djinit/templates/presets/predefined/apps/generic/views.py-tpl deleted file mode 100644 index ef85cf5..0000000 --- a/djinit/templates/presets/predefined/apps/generic/views.py-tpl +++ /dev/null @@ -1,62 +0,0 @@ -from rest_framework import viewsets, status -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated - -from [[ app_module ]].models.[[ app_name ]] import [[ model_class_name ]] -from [[ app_module ]].serializers.[[ app_name ]]_serializer import [[ model_class_name ]]Serializer -from [[ app_module ]].services.[[ app_name ]]_service import [[ model_class_name ]]Service - - -class [[ model_class_name ]]ViewSet(viewsets.ModelViewSet): - """ViewSet for [[ model_class_name ]] CRUD operations.""" - - queryset = [[ model_class_name ]].objects.all() - serializer_class = [[ model_class_name ]]Serializer - permission_classes = [IsAuthenticated] - - def list(self, request): - """List all [[ model_class_name ]] instances.""" - instances = [[ model_class_name ]]Service.get_all() - serializer = self.get_serializer(instances, many=True) - return Response(serializer.data) - - def retrieve(self, request, pk=None): - """Retrieve a single [[ model_class_name ]] instance.""" - instance = [[ model_class_name ]]Service.get_by_id(pk) - if not instance: - return Response( - {"error": "[[ model_class_name ]] not found"}, status=status.HTTP_404_NOT_FOUND - ) - serializer = self.get_serializer(instance) - return Response(serializer.data) - - def create(self, request): - """Create a new [[ model_class_name ]] instance.""" - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - instance = [[ model_class_name ]]Service.create(serializer.validated_data) - return Response( - self.get_serializer(instance).data, status=status.HTTP_201_CREATED - ) - - def update(self, request, pk=None): - """Update an existing [[ model_class_name ]] instance.""" - instance = [[ model_class_name ]]Service.get_by_id(pk) - if not instance: - return Response( - {"error": "[[ model_class_name ]] not found"}, status=status.HTTP_404_NOT_FOUND - ) - serializer = self.get_serializer(instance, data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - updated_instance = [[ model_class_name ]]Service.update(instance, serializer.validated_data) - return Response(self.get_serializer(updated_instance).data) - - def destroy(self, request, pk=None): - """Delete a [[ model_class_name ]] instance.""" - instance = [[ model_class_name ]]Service.get_by_id(pk) - if not instance: - return Response( - {"error": "[[ model_class_name ]] not found"}, status=status.HTTP_404_NOT_FOUND - ) - [[ model_class_name ]]Service.delete(instance) - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/djinit/templates/presets/predefined/apps/users/models.py-tpl b/djinit/templates/presets/predefined/apps/users/models.py-tpl deleted file mode 100644 index f6236bd..0000000 --- a/djinit/templates/presets/predefined/apps/users/models.py-tpl +++ /dev/null @@ -1,9 +0,0 @@ -from django.contrib.auth.models import AbstractUser -from django.db import models - - -class User(AbstractUser): - """Custom user model extending Django's AbstractUser.""" - - # Add custom fields here if needed - pass diff --git a/djinit/templates/presets/predefined/apps/users/serializers.py-tpl b/djinit/templates/presets/predefined/apps/users/serializers.py-tpl deleted file mode 100644 index d7a4f40..0000000 --- a/djinit/templates/presets/predefined/apps/users/serializers.py-tpl +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework import serializers - -from apps.users.models.user import User - - -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ["id", "username", "email", "first_name", "last_name"] diff --git a/djinit/templates/presets/predefined/apps/users/services.py-tpl b/djinit/templates/presets/predefined/apps/users/services.py-tpl deleted file mode 100644 index 036060b..0000000 --- a/djinit/templates/presets/predefined/apps/users/services.py-tpl +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Optional - -from apps.users.models.user import User - - -def get_user_by_id(user_id: int) -> Optional[User]: - try: - return User.objects.get(id=user_id) - except User.DoesNotExist: - return None diff --git a/djinit/templates/presets/predefined/apps/users/tests.py-tpl b/djinit/templates/presets/predefined/apps/users/tests.py-tpl deleted file mode 100644 index 1bde766..0000000 --- a/djinit/templates/presets/predefined/apps/users/tests.py-tpl +++ /dev/null @@ -1,12 +0,0 @@ -import pytest -from django.urls import reverse -from rest_framework.test import APIClient - - -@pytest.mark.django_db -def test_users_list_empty(): - client = APIClient() - url = reverse("v1:users-list") - response = client.get(url) - assert response.status_code == 200 - assert response.json() == [] diff --git a/djinit/templates/presets/predefined/apps/users/views.py-tpl b/djinit/templates/presets/predefined/apps/users/views.py-tpl deleted file mode 100644 index 8db2347..0000000 --- a/djinit/templates/presets/predefined/apps/users/views.py-tpl +++ /dev/null @@ -1,13 +0,0 @@ -from rest_framework import status, viewsets -from rest_framework.response import Response - -from apps.users.models.user import User -from apps.users.serializers.user_serializer import UserSerializer - - -class UserViewSet(viewsets.ReadOnlyModelViewSet): - queryset = User.objects.all() - serializer_class = UserSerializer - - def list(self, request, *args, **kwargs): - return super().list(request, *args, **kwargs) diff --git a/djinit/templates/presets/predefined/core/exceptions.py-tpl b/djinit/templates/presets/predefined/core/exceptions.py-tpl deleted file mode 100644 index bb426d3..0000000 --- a/djinit/templates/presets/predefined/core/exceptions.py-tpl +++ /dev/null @@ -1,5 +0,0 @@ -class ApplicationError(Exception): - """Base application exception.""" - - def __init__(self, message: str): - super().__init__(message) diff --git a/djinit/templates/presets/predefined/core/middleware/request_logger.py-tpl b/djinit/templates/presets/predefined/core/middleware/request_logger.py-tpl deleted file mode 100644 index d922ad1..0000000 --- a/djinit/templates/presets/predefined/core/middleware/request_logger.py-tpl +++ /dev/null @@ -1,14 +0,0 @@ -import logging - - -logger = logging.getLogger(__name__) - - -class RequestLoggerMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - logger.info("%s %s", request.method, request.path) - response = self.get_response(request) - return response diff --git a/djinit/templates/presets/predefined/core/mixins/timestamped_model.py-tpl b/djinit/templates/presets/predefined/core/mixins/timestamped_model.py-tpl deleted file mode 100644 index 6b2e70e..0000000 --- a/djinit/templates/presets/predefined/core/mixins/timestamped_model.py-tpl +++ /dev/null @@ -1,9 +0,0 @@ -from django.db import models - - -class TimestampedModel(models.Model): - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - abstract = True diff --git a/djinit/templates/presets/predefined/core/utils/responses.py-tpl b/djinit/templates/presets/predefined/core/utils/responses.py-tpl deleted file mode 100644 index f4b5269..0000000 --- a/djinit/templates/presets/predefined/core/utils/responses.py-tpl +++ /dev/null @@ -1,17 +0,0 @@ -# @IF include_header -"""generated with djinit""" - -# @ENDIF -from rest_framework.response import Response - - -def success(data=None, status_code=200): -# @IF include_docstrings """Return a successful response.""" -# @ENDIF - return Response({"success": True, "data": data}, status=status_code) - - -def error(message, status_code=400): -# @IF include_docstrings """Return an error response.""" -# @ENDIF - return Response({"success": False, "message": message}, status=status_code) diff --git a/djinit/templates/presets/unified/apps/core/apps.py-tpl b/djinit/templates/presets/unified/apps/core/apps.py-tpl deleted file mode 100644 index 1d202ca..0000000 --- a/djinit/templates/presets/unified/apps/core/apps.py-tpl +++ /dev/null @@ -1,8 +0,0 @@ -"""generated with djinit""" - -from django.apps import AppConfig - - -class CoreConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.core" diff --git a/djinit/templates/presets/unified/apps/core/models/__init__.py-tpl b/djinit/templates/presets/unified/apps/core/models/__init__.py-tpl deleted file mode 100644 index 89b2eb6..0000000 --- a/djinit/templates/presets/unified/apps/core/models/__init__.py-tpl +++ /dev/null @@ -1,4 +0,0 @@ -from .base import AbstractBaseModel -from .mixins import TimestampedModel - -__all__ = ["AbstractBaseModel", "TimestampedModel"] diff --git a/djinit/templates/presets/unified/apps/core/models/base.py-tpl b/djinit/templates/presets/unified/apps/core/models/base.py-tpl deleted file mode 100644 index a3970f9..0000000 --- a/djinit/templates/presets/unified/apps/core/models/base.py-tpl +++ /dev/null @@ -1,13 +0,0 @@ -"""generated with djinit""" - -from django.db import models - - -class AbstractBaseModel(models.Model): - """Abstract base model with common fields.""" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - abstract = True diff --git a/djinit/templates/presets/unified/apps/core/models/mixins.py-tpl b/djinit/templates/presets/unified/apps/core/models/mixins.py-tpl deleted file mode 100644 index fe40544..0000000 --- a/djinit/templates/presets/unified/apps/core/models/mixins.py-tpl +++ /dev/null @@ -1,13 +0,0 @@ -"""generated with djinit""" - -from django.db import models - - -class TimestampedModel(models.Model): - """Mixin for models that need timestamp fields.""" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - abstract = True diff --git a/djinit/templates/presets/unified/apps/core/utils/__init__.py-tpl b/djinit/templates/presets/unified/apps/core/utils/__init__.py-tpl deleted file mode 100644 index 155f5b4..0000000 --- a/djinit/templates/presets/unified/apps/core/utils/__init__.py-tpl +++ /dev/null @@ -1,3 +0,0 @@ -from .responses import success, error - -__all__ = ["success", "error"] diff --git a/djinit/templates/project/ci/github_actions-tpl b/djinit/templates/project/ci/github_actions-tpl deleted file mode 100644 index 5217107..0000000 --- a/djinit/templates/project/ci/github_actions-tpl +++ /dev/null @@ -1,109 +0,0 @@ -name: Django CI - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.12", "3.13"] - - services: - postgres: - image: postgres:15 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: test_db - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python {{ '{{ matrix.python-version }}' }} - uses: actions/setup-python@v5 - with: - python-version: {{ '{{ matrix.python-version }}' }} - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Install dependencies - run: uv sync - - - name: Run linting - run: uv run ruff check . - - - name: Format check - run: uv run ruff format --check . - - - name: Set up PostgreSQL - env: - DB_NAME: test_db - DB_USER: postgres - DB_PASSWORD: postgres - DB_HOST: localhost - DB_PORT: 5432 - run: | - echo "PostgreSQL is ready" - - - name: Run Django tests - env: - DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - DB_NAME: test_db - DB_USER: postgres - DB_PASSWORD: postgres - DB_HOST: localhost - DB_PORT: 5432 - run: | - uv run python manage.py check - uv run python manage.py test - - - name: Check migrations - env: - DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - DB_NAME: test_db - DB_USER: postgres - DB_PASSWORD: postgres - DB_HOST: localhost - DB_PORT: 5432 - run: | - uv run python manage.py makemigrations --check --dry-run - - security: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Install dependencies - run: uv sync - - - name: Run security check - run: uv run python manage.py check --deploy diff --git a/djinit/templates/project/ci/gitlab_ci-tpl b/djinit/templates/project/ci/gitlab_ci-tpl deleted file mode 100644 index 3d3bc61..0000000 --- a/djinit/templates/project/ci/gitlab_ci-tpl +++ /dev/null @@ -1,133 +0,0 @@ -# GitLab CI/CD configuration for [[ project_name ]] - -stages: - - test - - lint - - security - -variables: - POSTGRES_DB: test_db - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/test_db" - -# Use Docker-in-Docker -services: - - postgres:15 - -before_script: - - python --version - - pip install uv - - uv sync - -# Lint stage -lint: - stage: lint - image: python:3.13 - script: - - uv run ruff check . - - uv run ruff format --check . - only: - - branches - - merge_requests - -# Test stage -test: - stage: test - image: python:3.13 - variables: - DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - script: - - uv run python manage.py check - - uv run python manage.py migrate - - uv run python manage.py test - only: - - branches - - merge_requests - -# Multiple Python versions -test:python310: - stage: test - image: python:3.10 - variables: - DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - script: - - pip install uv - - uv sync - - uv run python manage.py check - - uv run python manage.py migrate - - uv run python manage.py test - only: - - branches - - merge_requests - -test:python311: - stage: test - image: python:3.11 - variables: - DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - script: - - pip install uv - - uv sync - - uv run python manage.py check - - uv run python manage.py migrate - - uv run python manage.py test - only: - - branches - - merge_requests - -test:python312: - stage: test - image: python:3.12 - variables: - DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - script: - - pip install uv - - uv sync - - uv run python manage.py check - - uv run python manage.py migrate - - uv run python manage.py test - only: - - branches - - merge_requests - -test:python313: - stage: test - image: python:3.13 - variables: - DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - script: - - pip install uv - - uv sync - - uv run python manage.py check - - uv run python manage.py migrate - - uv run python manage.py test - only: - - branches - - merge_requests - -# Security check -security: - stage: security - image: python:3.13 - variables: - DJANGO_SETTINGS_MODULE: [[ project_name ]].settings.development - SECRET_KEY: test-secret-key-for-ci - script: - - pip install uv - - uv sync - - uv run python manage.py check --deploy - - uv run python manage.py validate_templates - only: - - main - - develop diff --git a/djinit/templates/project/env_sample-tpl b/djinit/templates/project/env_sample-tpl deleted file mode 100644 index 382da28..0000000 --- a/djinit/templates/project/env_sample-tpl +++ /dev/null @@ -1,32 +0,0 @@ -# Django settings -DJANGO_SETTINGS_MODULE=[[ project_name ]].settings.development -SECRET_KEY= your-secret-key -ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com - -# @IF use_database_url -# Database URL (recommended for production) -# @IF database_type == 'mysql' -DATABASE_URL=mysql://user:password@localhost:3306/[[ project_name ]] -# @ELSE -DATABASE_URL=postgres://user:password@localhost:5432/[[ project_name ]] -# @ENDIF -# @ELSE -# Database (individual parameters) -DB_NAME= -DB_USER= -DB_PASSWORD= -DB_HOST= -DB_PORT= -# @ENDIF - -# Frontend URL -FRONTEND_URL=http://frontend.your-domain.com - -# Email settings -EMAIL_HOST= -EMAIL_PORT= -EMAIL_USE_TLS= -EMAIL_HOST_USER= -EMAIL_HOST_PASSWORD= -DEFAULT_FROM_EMAIL= -SERVER_EMAIL= diff --git a/djinit/templates/project/gitignore-tpl b/djinit/templates/project/gitignore-tpl deleted file mode 100644 index 2b5243c..0000000 --- a/djinit/templates/project/gitignore-tpl +++ /dev/null @@ -1,93 +0,0 @@ -#--------------------------------------------------# -# The following was generated by djinit: # -#--------------------------------------------------# - -# Python -*.py[cod] -*$py.class -*.so -__pycache__/ -*.egg-info/ -dist/ -build/ -.eggs/ -*.egg - -# Django -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal -*.sqlite3 - -# Environment -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# IDEs -.vscode/ -.idea/ -*.swp -*.swo -*~ -.project -.pydevproject -.settings/ -.ropeproject/ - -# OS -.DS_Store -*.DS_Store -Thumbs.db -desktop.ini - -# Django specific -media/ -staticfiles/ -staticfiles/ -static_root/ -/static/ - -# Testing -.coverage -htmlcov/ -.pytest_cache/ -.tox/ -coverage.xml -*.cover -.hypothesis/ -.cache - -# Logs -logs/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Node modules (if using frontend tools) -node_modules/ - -# Secret keys -secret.txt -secrets.json - -# Jupyter Notebook -.ipynb_checkpoints - -# Backup files -*.bak -*.tmp -*~ - -# Database -*.db -*.sql - -# Cloud -.cloud diff --git a/djinit/templates/project/init.py-tpl b/djinit/templates/project/init.py-tpl deleted file mode 100644 index e69de29..0000000 diff --git a/djinit/templates/project/justfile-tpl b/djinit/templates/project/justfile-tpl deleted file mode 100644 index 127e77d..0000000 --- a/djinit/templates/project/justfile-tpl +++ /dev/null @@ -1,219 +0,0 @@ -# Justfile for [[ project_name ]] -# Task runner for Django development -# Install just: https://github.com/casey/just - -# Default task: show available commands -default: - @just --list - -# Run development server -dev: - uv run python manage.py runserver - -# 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{{ '}}' }} - -# Create migrations for all apps -makemigrations: - uv run python manage.py makemigrations - -# Create migrations for specific app -makemigrations-app APP: - uv run python manage.py makemigrations {{ '{{' }}APP{{ '}}' }} - -# Run all migrations -migrate: - uv run python manage.py migrate - -# Reset database (delete all migrations) -reset-db: - uv run python manage.py flush --noinput - uv run python manage.py migrate - -# Create superuser -createsuperuser: - uv run python manage.py createsuperuser - -# Collect static files -collectstatic: - uv run python manage.py collectstatic --noinput - -# Run Django shell -shell: - uv run python manage.py shell - -# Run Django shell plus (IPython) -shell-plus: - uv run python manage.py shell_plus - -# Run tests -test: - uv run python manage.py test - -# Run tests with coverage -test-coverage: - uv run pytest --cov=. --cov-report=html --cov-report=term - -# Lint code with ruff -lint: - uv run ruff check . - -# Format code with ruff -format: - uv run ruff format . - -# Lint and format code -style: - uv run ruff check . - uv run ruff format . - -# Check security issues -check: - uv run python manage.py check - -# Check security issues (production settings) -check-deploy: - uv run python manage.py check --deploy - -# Show all database migrations status -showmigrations: - uv run python manage.py showmigrations - -# Sync dependencies (uv equivalent of pip freeze) -requirements: - uv pip compile - -# Run all linting and checks -ci: - uv run ruff check . - uv run ruff format --check . - uv run python manage.py check - -# Start interactive shell -shell-ipython: - uv run python manage.py shell_plus - -# Clear all cache -clear-cache: - uv run python manage.py clear_cache - -# Show Django version -version: - uv run python manage.py version - -# Run database shell -dbshell: - uv run python manage.py dbshell - -# Start production server with gunicorn -server: - uv run gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:8000 - -# Start production server with gunicorn workers -server-prod WORKERS="4": - uv run gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:8000 --workers {{ '{{' }}WORKERS{{ '}}' }} - -# Start production server with gunicorn and reload on file changes -server-watch: - uv run gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:8000 --reload - -# Clean Python cache files -clean: - find . -type d -name "__pycache__" -exec rm -rf {} + - find . -type f -name "*.pyc" -delete - find . -type f -name "*.pyo" -delete - find . -type f -name ".coverage" -delete - find . -type d -name "*.egg-info" -exec rm -rf {} + - rm -rf htmlcov/ - rm -rf .pytest_cache/ - -# Pin dependencies in pyproject.toml using versions from uv.lock, keeping >= and extras -pin-deps: - #!/usr/bin/env -S uv run python - import tomllib, re - from pathlib import Path - - lock = Path("uv.lock") - if not lock.exists(): - print("uv.lock not found — run `uv sync` first") - raise SystemExit(0) - - data = tomllib.loads(lock.read_text()) - packages = {p["name"].lower(): p["version"] for p in data.get("package", [])} - - py = Path("pyproject.toml") - lines = py.read_text().splitlines() - - pattern = re.compile(r'^(\s*)"([^"]+)"(,?)') - - out = [] - for line in lines: - m = pattern.match(line) - if not m: - out.append(line) - continue - - indent, dep, comma = m.groups() - - base = dep - extras = "" - operator = ">=" - version = None - - # extras: django[postgres] - if "[" in base and "]" in base: - before, after = base.split("[", 1) - extras = "[" + after.split("]")[0] + "]" - base = before - - # detect >= or == - if ">=" in dep: - operator = ">=" - base, version = dep.split(">=") - base = base.strip() - elif "==" in dep: - operator = "==" - base, version = dep.split("==") - base = base.strip() - else: - base = dep - - base_lower = base.lower() - new_version = packages.get(base_lower) - - if new_version: - new_dep = f'{base}{extras}{operator}{new_version}' - line = f'{indent}"{new_dep}"{comma}' - - out.append(line) - - py.write_text("\n".join(out) + "\n") - print("Pinned dependencies (>= preserved, extras preserved).") - - -# Setup project for development (using uv) -setup: - uv sync - just pin-deps - uv run python manage.py migrate - uv run python manage.py createsuperuser - -# Complete setup including static files -setup-complete: - uv sync - uv run python manage.py migrate - uv run python manage.py collectstatic --noinput - uv run python manage.py createsuperuser - -# Install a new package -install PACKAGE: - uv add {{ '{{' }}PACKAGE{{ '}}' }} - -# Install a dev package -install-dev PACKAGE: - uv add --dev {{ '{{' }}PACKAGE{{ '}}' }} - -# Remove a package -remove PACKAGE: - uv remove {{ '{{' }}PACKAGE{{ '}}' }} diff --git a/djinit/templates/project/manage.py-tpl b/djinit/templates/project/manage.py-tpl deleted file mode 100644 index ba6f5c7..0000000 --- a/djinit/templates/project/manage.py-tpl +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python - -"""generated with djinit""" - -"""Django's command-line utility for administrative tasks.""" -import os -import sys - -import environ - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "[[ project_name ]].settings.production")) - - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - env = environ.Env() - # Read .env file, if it exists - environ.Env.read_env() - - main() diff --git a/djinit/templates/project/procfile-tpl b/djinit/templates/project/procfile-tpl deleted file mode 100644 index fc01178..0000000 --- a/djinit/templates/project/procfile-tpl +++ /dev/null @@ -1,20 +0,0 @@ -# Procfile for [[ project_name ]] -# Used for deploying to PaaS platforms like Heroku, Railway, Render, etc. - -# Web server process (using Gunicorn) -# Adjust workers based on your needs: (2 x CPU cores) + 1 -web: gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info - -# Alternative web server configurations (uncomment to use): -# web: gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120 -# web: gunicorn [[ project_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120 - -# Release task (runs migrations before deployment) -release: python manage.py migrate --noinput && python manage.py collectstatic --noinput - -# Uncomment below to run scheduled tasks (if using Celery) -# worker: celery -A [[ project_name ]] worker --loglevel=info -# beat: celery -A [[ project_name ]] beat --loglevel=info - -# Development server (for local testing with Foreman/Overmind) -# web: python manage.py runserver 0.0.0.0:$PORT diff --git a/djinit/templates/project/pyproject.toml-tpl b/djinit/templates/project/pyproject.toml-tpl deleted file mode 100644 index e9e49a3..0000000 --- a/djinit/templates/project/pyproject.toml-tpl +++ /dev/null @@ -1,46 +0,0 @@ -[project] -name = "[[ package_name ]]" -version = "0.1.0" -description = "" -readme = "README.md" -requires-python = ">=3.13" -license = { text = "MIT" } -dependencies = [ - "django", - "django-environ", - "django-jazzmin", - "djangorestframework", - "djangorestframework-simplejwt", - "drf-spectacular", - "django-cors-headers", - "whitenoise", -# @IF database_type == 'mysql' - "mysqlclient", -# @ENDIF -# @IF database_type == 'postgresql' - "psycopg2-binary", -# @ENDIF - "gunicorn", -] - -[tool.ruff] -line-length = 88 -target-version = "py38" - -[tool.ruff.lint] -select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "UP", # pyupgrade -] -ignore = [ - "E501", # line too long, handled by formatter - "B008", # do not perform function calls in argument defaults -] - -[tool.ruff.lint.isort] -known-first-party = ["[[ project_name ]]"] diff --git a/djinit/templates/project/readme.md-tpl b/djinit/templates/project/readme.md-tpl deleted file mode 100644 index 9c28632..0000000 --- a/djinit/templates/project/readme.md-tpl +++ /dev/null @@ -1,201 +0,0 @@ -# [[ project_name ]] - -A Django project folder structure is generated 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 -- `just install ` - Install package -- `just remove ` - Remove package -- `just server` - Run production server -- `just ci` - Run all linting and checks -- `just test-coverage` - Run tests with coverage - -## 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 -├── uv.lock -├── .env.sample -├── justfile -├── Procfile -├── runtime.txt -└── README.md -# @ELSEIF 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 -├── uv.lock -├── .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 -├── uv.lock -├── .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/ diff --git a/djinit/templates/project/requirements-tpl b/djinit/templates/project/requirements-tpl deleted file mode 100644 index cb48ff9..0000000 --- a/djinit/templates/project/requirements-tpl +++ /dev/null @@ -1,19 +0,0 @@ -#--------------------------------------------------# -# 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 -dj-database-url # @IF use_database_url diff --git a/djinit/templates/project/runtime_txt-tpl b/djinit/templates/project/runtime_txt-tpl deleted file mode 100644 index 34ffefa..0000000 --- a/djinit/templates/project/runtime_txt-tpl +++ /dev/null @@ -1 +0,0 @@ -python-[[ python_version ]] -- 2.43.0 From 68f1b44eab9d8a98f3764b51fe74dd8c33bc4b80 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 1 Mar 2026 00:27:47 +0545 Subject: [PATCH 21/24] fix: fixed secret key not generated --- src/djinit/cli.py | 8 +++++--- src/djinit/creators/project.py | 2 +- src/djinit/utils/django.py | 29 +++++++++++++++++++++++------ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/djinit/cli.py b/src/djinit/cli.py index 5b1cf51..4f33abd 100644 --- a/src/djinit/cli.py +++ b/src/djinit/cli.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) app = typer.Typer( help="djinit - A CLI tool to set up Django projects with best practices.", add_completion=False, - no_args_is_help=True, + no_args_is_help=False, ) @@ -33,8 +33,9 @@ def version_callback(value: bool): raise typer.Exit() -@app.callback() +@app.callback(invoke_without_command=True) def main( + ctx: typer.Context, version: Annotated[ Optional[bool], typer.Option("--version", "-v", callback=version_callback, is_eager=True, help="Show the version and exit."), @@ -43,7 +44,8 @@ def main( """ djinit: Django Project Initializer """ - pass + if ctx.invoked_subcommand is None: + setup() @app.command("secret") diff --git a/src/djinit/creators/project.py b/src/djinit/creators/project.py index b0a663b..57ac51b 100644 --- a/src/djinit/creators/project.py +++ b/src/djinit/creators/project.py @@ -27,7 +27,7 @@ class ProjectCreator(BaseService): os.makedirs(self.project_root, exist_ok=True) unified = self.metadata.get("unified_structure", False) - DjangoHelper.startproject(self.module_name, self.project_root, unified=unified) + DjangoHelper.startproject(self.module_name, self.project_root, unified=unified, metadata=self.metadata) UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!") def _get_apps_base_dir(self) -> str: diff --git a/src/djinit/utils/django.py b/src/djinit/utils/django.py index 1f74d6e..13a6d28 100644 --- a/src/djinit/utils/django.py +++ b/src/djinit/utils/django.py @@ -14,8 +14,10 @@ class DjangoHelper: DJANGO_VERSION = DJANGO_VERSION @staticmethod - def startproject(project_name: str, directory: str, unified: bool = False) -> None: + def startproject(project_name: str, directory: str, unified: bool = False, metadata: dict = None) -> None: try: + from djinit.utils.secretkey import generate_secret_key + os.makedirs(directory, exist_ok=True) manage_py_path = os.path.join(directory, "manage.py") @@ -33,11 +35,26 @@ class DjangoHelper: settings_dir = os.path.join(project_config_dir, "settings") CommonUtils.create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py") - base_context = {"project_name": project_name, "app_names": []} + # Generate secret key for development + secret_key = generate_secret_key() + metadata = metadata or {} + + base_context = { + "project_name": project_name, + "app_names": [], + "use_database_url": metadata.get("use_database_url", True), + "database_type": metadata.get("database_type", "postgresql"), + } + + dev_context = {"secret_key": secret_key} + + # Production context also needs database info + prod_context = base_context.copy() + settings_files = [ ("base.py", "config/settings/base.py-tpl", base_context), - ("development.py", "config/settings/development.py-tpl", {}), - ("production.py", "config/settings/production.py-tpl", {}), + ("development.py", "config/settings/development.py-tpl", dev_context), + ("production.py", "config/settings/production.py-tpl", prod_context), ] CommonUtils.create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/") @@ -48,8 +65,8 @@ class DjangoHelper: } project_files = [ ("urls.py", "config/urls.py-tpl", urls_context), - ("wsgi.py", "config/wsgi.py-tpl", {}), - ("asgi.py", "config/asgi.py-tpl", {}), + ("wsgi.py", "config/wsgi.py-tpl", {"project_name": project_name}), + ("asgi.py", "config/asgi.py-tpl", {"project_name": project_name}), ] CommonUtils.create_files_from_templates(project_config_dir, project_files, f"{project_name}/") -- 2.43.0 From c42e0635b89478b996cd0ad7f1416b97b8990c84 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 1 Mar 2026 00:45:29 +0545 Subject: [PATCH 22/24] update templates --- src/djinit/templates/config/settings/production.py-tpl | 10 +++++----- src/djinit/templates/project/env_sample-tpl | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/djinit/templates/config/settings/production.py-tpl b/src/djinit/templates/config/settings/production.py-tpl index 7d58363..d22ba79 100644 --- a/src/djinit/templates/config/settings/production.py-tpl +++ b/src/djinit/templates/config/settings/production.py-tpl @@ -42,13 +42,13 @@ CORS_ALLOW_PRIVATE_NETWORK = False # Email settings EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = env('EMAIL_HOST') +EMAIL_HOST = env('EMAIL_HOST', default="smtp.gmail.com") EMAIL_PORT = env.int('EMAIL_PORT', default=587) EMAIL_USE_TLS = env.bool('EMAIL_USE_TLS', default=True) -EMAIL_HOST_USER = env('EMAIL_HOST_USER') -EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD') -DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER') -SERVER_EMAIL = env('EMAIL_HOST_USER') +EMAIL_HOST_USER = env('EMAIL_HOST_USER', default="") +EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD', default="") +DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER', default="") +SERVER_EMAIL = env('EMAIL_HOST_USER', default="") # Security settings for production SECURE_SSL_REDIRECT = True diff --git a/src/djinit/templates/project/env_sample-tpl b/src/djinit/templates/project/env_sample-tpl index 382da28..bdeca01 100644 --- a/src/djinit/templates/project/env_sample-tpl +++ b/src/djinit/templates/project/env_sample-tpl @@ -1,6 +1,6 @@ # Django settings DJANGO_SETTINGS_MODULE=[[ project_name ]].settings.development -SECRET_KEY= your-secret-key +SECRET_KEY=your-secret-key ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com # @IF use_database_url -- 2.43.0 From 992688641eec040b2c1f6b0f4f641d2940a51b54 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 1 Mar 2026 00:48:50 +0545 Subject: [PATCH 23/24] update templates --- src/djinit/templates/project/justfile-tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/djinit/templates/project/justfile-tpl b/src/djinit/templates/project/justfile-tpl index 127e77d..a67dbd0 100644 --- a/src/djinit/templates/project/justfile-tpl +++ b/src/djinit/templates/project/justfile-tpl @@ -82,7 +82,7 @@ showmigrations: # Sync dependencies (uv equivalent of pip freeze) requirements: - uv pip compile + uv run python -c "import tomllib; p = tomllib.load(open('pyproject.toml', 'rb')); print('\n'.join(p['project']['dependencies']))" > requirements.txt # Run all linting and checks ci: -- 2.43.0 From 6c56689d8c5ae77c535eb113761391e7b0d00c46 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 1 Mar 2026 00:51:19 +0545 Subject: [PATCH 24/24] chore: Remove space after equals sign in SECRET_KEY assignment. --- src/djinit/templates/project/env_sample-tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/djinit/templates/project/env_sample-tpl b/src/djinit/templates/project/env_sample-tpl index 30ec347..c1a6cf7 100644 --- a/src/djinit/templates/project/env_sample-tpl +++ b/src/djinit/templates/project/env_sample-tpl @@ -1,6 +1,6 @@ # Django settings DJANGO_SETTINGS_MODULE={{ project_name }}.settings.development -SECRET_KEY= your-secret-key +SECRET_KEY=your-secret-key ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com # @IF use_database_url -- 2.43.0