karni-cli 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. karni_cli/__init__.py +3 -0
  2. karni_cli/_agent/__init__.py +82 -0
  3. karni_cli/_agent/__main__.py +44 -0
  4. karni_cli/_agent/check.py +658 -0
  5. karni_cli/_agent/history.py +328 -0
  6. karni_cli/_agent/history_chronicle.py +246 -0
  7. karni_cli/_agent/overrides.py +194 -0
  8. karni_cli/_agent/rules.py +673 -0
  9. karni_cli/_changelog/0.2.0.toml +304 -0
  10. karni_cli/_changelog/0.3.0.toml +25 -0
  11. karni_cli/_changelog/__init__.py +248 -0
  12. karni_cli/_errorcodes/__init__.py +1171 -0
  13. karni_cli/_errorcodes/__main__.py +34 -0
  14. karni_cli/_errorcodes/links.py +217 -0
  15. karni_cli/_templates/project/cookiecutter.json +8 -0
  16. karni_cli/_templates/project/{{cookiecutter.project_slug}}/.dockerignore +7 -0
  17. karni_cli/_templates/project/{{cookiecutter.project_slug}}/.env.example +85 -0
  18. karni_cli/_templates/project/{{cookiecutter.project_slug}}/.gitignore +10 -0
  19. karni_cli/_templates/project/{{cookiecutter.project_slug}}/Caddyfile +22 -0
  20. karni_cli/_templates/project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
  21. karni_cli/_templates/project/{{cookiecutter.project_slug}}/Makefile +52 -0
  22. karni_cli/_templates/project/{{cookiecutter.project_slug}}/README.md +169 -0
  23. karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/__init__.py +16 -0
  24. karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/asgi.py +14 -0
  25. karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/celery.py +17 -0
  26. karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/settings.py +28 -0
  27. karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/urls.py +31 -0
  28. karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/wsgi.py +14 -0
  29. karni_cli/_templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +126 -0
  30. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/.dockerignore +2 -0
  31. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/.jscpd.json +9 -0
  32. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/.npmrc +7 -0
  33. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/Dockerfile.dev +17 -0
  34. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/eslint.config.js +32 -0
  35. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/index.html +14 -0
  36. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/package.json +43 -0
  37. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/postcss.config.js +6 -0
  38. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/public/favicon.svg +6 -0
  39. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/App.tsx +18 -0
  40. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/api/health.ts +21 -0
  41. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/index.css +11 -0
  42. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/layout.tsx +51 -0
  43. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/main.tsx +11 -0
  44. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/pages/home.tsx +28 -0
  45. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/routes.tsx +24 -0
  46. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/vite-env.d.ts +1 -0
  47. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/tailwind.config.ts +13 -0
  48. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/tsconfig.json +19 -0
  49. karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/vite.config.ts +18 -0
  50. karni_cli/_templates/project/{{cookiecutter.project_slug}}/manage.py +23 -0
  51. karni_cli/_templates/project/{{cookiecutter.project_slug}}/pyproject.toml +31 -0
  52. karni_cli/_templates/project/{{cookiecutter.project_slug}}/tests/__init__.py +0 -0
  53. karni_cli/_templates/project/{{cookiecutter.project_slug}}/tests/test_api_schema.py +17 -0
  54. karni_cli/_templates/project/{{cookiecutter.project_slug}}/tests/test_smoke.py +11 -0
  55. karni_cli/adapters.py +373 -0
  56. karni_cli/agent_adapters.toml +160 -0
  57. karni_cli/agent_data.py +260 -0
  58. karni_cli/brand.py +55 -0
  59. karni_cli/clidoc.py +254 -0
  60. karni_cli/commands/__init__.py +0 -0
  61. karni_cli/commands/add.py +144 -0
  62. karni_cli/commands/beat.py +36 -0
  63. karni_cli/commands/check.py +101 -0
  64. karni_cli/commands/config.py +81 -0
  65. karni_cli/commands/context.py +171 -0
  66. karni_cli/commands/createsuperuser.py +28 -0
  67. karni_cli/commands/doctor.py +136 -0
  68. karni_cli/commands/explain.py +60 -0
  69. karni_cli/commands/make.py +55 -0
  70. karni_cli/commands/manage_passthrough.py +28 -0
  71. karni_cli/commands/mcp.py +20 -0
  72. karni_cli/commands/migrate.py +23 -0
  73. karni_cli/commands/new.py +106 -0
  74. karni_cli/commands/routes.py +20 -0
  75. karni_cli/commands/rules.py +139 -0
  76. karni_cli/commands/run_tests.py +38 -0
  77. karni_cli/commands/schedule.py +40 -0
  78. karni_cli/commands/seed.py +34 -0
  79. karni_cli/commands/serve.py +50 -0
  80. karni_cli/commands/shell.py +28 -0
  81. karni_cli/commands/sync_roles.py +32 -0
  82. karni_cli/commands/types.py +72 -0
  83. karni_cli/commands/upgrade.py +231 -0
  84. karni_cli/commands/worker.py +63 -0
  85. karni_cli/config.py +142 -0
  86. karni_cli/environment.py +29 -0
  87. karni_cli/errorcodes.py +377 -0
  88. karni_cli/errordoc.py +227 -0
  89. karni_cli/generators/__init__.py +67 -0
  90. karni_cli/generators/apitest.py +206 -0
  91. karni_cli/generators/app.py +103 -0
  92. karni_cli/generators/command.py +98 -0
  93. karni_cli/generators/component.py +115 -0
  94. karni_cli/generators/core.py +319 -0
  95. karni_cli/generators/event.py +57 -0
  96. karni_cli/generators/factory.py +88 -0
  97. karni_cli/generators/fields.py +343 -0
  98. karni_cli/generators/introspect.py +263 -0
  99. karni_cli/generators/listener.py +168 -0
  100. karni_cli/generators/middleware.py +95 -0
  101. karni_cli/generators/migration.py +82 -0
  102. karni_cli/generators/model.py +235 -0
  103. karni_cli/generators/module.py +255 -0
  104. karni_cli/generators/naming.py +100 -0
  105. karni_cli/generators/page.py +373 -0
  106. karni_cli/generators/pysource.py +304 -0
  107. karni_cli/generators/schedule.py +174 -0
  108. karni_cli/generators/seeder.py +86 -0
  109. karni_cli/generators/serializer.py +165 -0
  110. karni_cli/generators/service.py +68 -0
  111. karni_cli/generators/task.py +84 -0
  112. karni_cli/generators/viewset.py +179 -0
  113. karni_cli/history.py +59 -0
  114. karni_cli/local_state.py +73 -0
  115. karni_cli/main.py +104 -0
  116. karni_cli/mcp_server.py +103 -0
  117. karni_cli/project.py +248 -0
  118. karni_cli/rules_render.py +86 -0
  119. karni_cli/scaffold.py +197 -0
  120. karni_cli/settings_edit.py +128 -0
  121. karni_cli/tips.py +53 -0
  122. karni_cli/typegen.py +101 -0
  123. karni_cli/upgrades/__init__.py +28 -0
  124. karni_cli/upgrades/changelog.py +123 -0
  125. karni_cli/upgrades/codemod.py +125 -0
  126. karni_cli/upgrades/engine.py +224 -0
  127. karni_cli/upgrades/legacy_agent_files.py +138 -0
  128. karni_cli/upgrades/python.py +670 -0
  129. karni_cli/upgrades/transforms/rename-export.cjs +96 -0
  130. karni_cli/upgrades/typescript.py +160 -0
  131. karni_cli/upgrades/v0_2/__init__.py +1 -0
  132. karni_cli/upgrades/v0_2/agent_guidance.py +269 -0
  133. karni_cli/upgrades/v1_0/__init__.py +5 -0
  134. karni_cli/upgrades/v1_0/all_objects_to_without.py +18 -0
  135. karni_cli/upgrades/v1_0/all_with_deleted_to_without.py +13 -0
  136. karni_cli/upgrades/v1_0/deleted_only_to_without_dead.py +13 -0
  137. karni_cli/upgrades/v1_0/object_permission_classes_to_access.py +16 -0
  138. karni_cli/upgrades/v1_0/related_lookups_to_optimize.py +15 -0
  139. karni_cli/upgrades/v1_0/scoped_related_lookups_to_optimize.py +15 -0
  140. karni_cli/upgrades/v1_0/soft_delete_manager_mixin_to_scoped.py +14 -0
  141. karni_cli-0.3.1.dist-info/METADATA +191 -0
  142. karni_cli-0.3.1.dist-info/RECORD +145 -0
  143. karni_cli-0.3.1.dist-info/WHEEL +4 -0
  144. karni_cli-0.3.1.dist-info/entry_points.txt +2 -0
  145. karni_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,34 @@
1
+ """``python -m karni.errorcodes CODE [--json]``: one registry entry, modules included.
2
+
3
+ karni-cli's ``karni explain`` runs this in the project interpreter for codes
4
+ it does not bundle (an installed module's, such as ``PAY004``).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+
13
+ from karni.errorcodes import get
14
+
15
+
16
+ def main(argv: list[str] | None = None) -> int:
17
+ parser = argparse.ArgumentParser(prog="python -m karni.errorcodes")
18
+ parser.add_argument("code")
19
+ parser.add_argument("--json", action="store_true")
20
+ args = parser.parse_args(argv)
21
+ entry = get(args.code)
22
+ if entry is None:
23
+ print(f"Unknown code {args.code!r}.", file=sys.stderr)
24
+ return 1
25
+ data = entry.as_dict()
26
+ if args.json:
27
+ print(json.dumps(data))
28
+ else:
29
+ print("\n".join(f"{key}: {value}" for key, value in data.items()))
30
+ return 0
31
+
32
+
33
+ if __name__ == "__main__":
34
+ sys.exit(main())
@@ -0,0 +1,217 @@
1
+ """Every external URL and address Karni publishes, in one place.
2
+
3
+ Error messages, deprecation warnings, the changelog, the CLI, the docs site,
4
+ the package metadata and the README all point at these. Change them here,
5
+ then run ``python -m karni.links sync`` to rewrite the files that cannot
6
+ import Python (``pyproject.toml``, ``package.json``, ``mkdocs.yml``, the
7
+ Markdown pages); ``python -m karni.links check`` (run by the test suite)
8
+ fails while any of them still carries an old value.
9
+
10
+ Stdlib only: ``karni.errorcodes`` imports this module, and karni-cli
11
+ bundles both so ``karni explain`` works without Django or a project.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import re
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import Final
21
+
22
+ __all__ = [
23
+ "DOCS_BASE_URL",
24
+ "ISSUES_URL",
25
+ "REPO_SLUG",
26
+ "REPO_URL",
27
+ "SECURITY_ADVISORY_URL",
28
+ "SUPPORT_EMAIL",
29
+ "docs_page",
30
+ ]
31
+
32
+ #: ``owner/name`` on GitHub.
33
+ REPO_SLUG: Final[str] = "cbsshekhawat18-lab/karni"
34
+ #: The repository.
35
+ REPO_URL: Final[str] = f"https://github.com/{REPO_SLUG}"
36
+ #: The documentation site; ends with a slash. Pages are ``DOCS_BASE_URL + "page/"``.
37
+ DOCS_BASE_URL: Final[str] = "https://cbsshekhawat18-lab.github.io/karni/"
38
+ #: Where bugs go.
39
+ ISSUES_URL: Final[str] = f"{REPO_URL}/issues"
40
+ #: Private vulnerability reports.
41
+ SECURITY_ADVISORY_URL: Final[str] = f"{REPO_URL}/security/advisories/new"
42
+ #: The maintainer's address for security reports that cannot use GitHub.
43
+ SUPPORT_EMAIL: Final[str] = "cbsshekhawat18@gmail.com"
44
+
45
+
46
+ def docs_page(page: str, anchor: str = "") -> str:
47
+ """``docs/<page>.md#anchor`` as the published URL (``…/page/#anchor``)."""
48
+ path = page.removesuffix(".md").strip("/")
49
+ url = f"{DOCS_BASE_URL}{path}/" if path else DOCS_BASE_URL
50
+ return f"{url}#{anchor}" if anchor else url
51
+
52
+
53
+ # The files that carry these values literally, and the pattern each one uses.
54
+ # `sync` rewrites them; `check` reports the ones that drifted.
55
+
56
+ _PYPROJECT_URLS = {
57
+ "Homepage": lambda: REPO_URL,
58
+ "Documentation": lambda: DOCS_BASE_URL,
59
+ "Repository": lambda: REPO_URL,
60
+ "Issues": lambda: ISSUES_URL,
61
+ "Changelog": lambda: f"{REPO_URL}/tree/main/docs/migrations",
62
+ }
63
+
64
+ _PACKAGE_JSON = {
65
+ "packages/karni-ui/package.json": "packages/karni-ui",
66
+ "modules/karni-payments/ui/package.json": "modules/karni-payments/ui",
67
+ }
68
+
69
+ _MKDOCS = {
70
+ "site_url": lambda: DOCS_BASE_URL,
71
+ "repo_url": lambda: REPO_URL,
72
+ "repo_name": lambda: REPO_SLUG,
73
+ }
74
+
75
+
76
+ def repo_root() -> Path | None:
77
+ for parent in Path(__file__).resolve().parents:
78
+ if (parent / "mkdocs.yml").is_file() and (parent / "packages").is_dir():
79
+ return parent
80
+ return None
81
+
82
+
83
+ def _sync_pyproject(path: Path, dry_run: bool) -> list[str]:
84
+ text = path.read_text()
85
+ out = text
86
+ for key, value in _PYPROJECT_URLS.items():
87
+ out = re.sub(rf'^{key} = "[^"]*"$', f'{key} = "{value()}"', out, flags=re.M)
88
+ return _write(path, text, out, dry_run)
89
+
90
+
91
+ def _sync_package_json(path: Path, subdir: str, dry_run: bool) -> list[str]:
92
+ text = path.read_text()
93
+ data = json.loads(text)
94
+ data["repository"] = {"type": "git", "url": f"git+{REPO_URL}.git", "directory": subdir}
95
+ data["homepage"] = f"{REPO_URL}/tree/main/{subdir}#readme"
96
+ data["bugs"] = {"url": ISSUES_URL}
97
+ out = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
98
+ return _write(path, text, out, dry_run)
99
+
100
+
101
+ def _sync_mkdocs(path: Path, dry_run: bool) -> list[str]:
102
+ text = path.read_text()
103
+ out = text
104
+ for key, value in _MKDOCS.items():
105
+ out = re.sub(rf"^{key}: .*$", f"{key}: {value()}", out, flags=re.M)
106
+ return _write(path, text, out, dry_run)
107
+
108
+
109
+ def _write(path: Path, before: str, after: str, dry_run: bool) -> list[str]:
110
+ if before == after:
111
+ return []
112
+ if not dry_run:
113
+ path.write_text(after)
114
+ return [str(path)]
115
+
116
+
117
+ def _sync_eslint_docs(path: Path, dry_run: bool) -> list[str]:
118
+ text = path.read_text()
119
+ out = re.sub(
120
+ r'^export const DOCS_BASE_URL = "[^"]*";$',
121
+ f'export const DOCS_BASE_URL = "{DOCS_BASE_URL}";',
122
+ text,
123
+ flags=re.M,
124
+ )
125
+ return _write(path, text, out, dry_run)
126
+
127
+
128
+ def sync(root: Path, *, dry_run: bool = False) -> list[str]:
129
+ """Rewrite the metadata files from these constants; returns the files that changed."""
130
+ changed: list[str] = []
131
+ for rel in ("packages/karni", "packages/karni-cli", "modules/karni-payments"):
132
+ changed += _sync_pyproject(root / rel / "pyproject.toml", dry_run)
133
+ for rel, subdir in _PACKAGE_JSON.items():
134
+ changed += _sync_package_json(root / rel, subdir, dry_run)
135
+ changed += _sync_mkdocs(root / "mkdocs.yml", dry_run)
136
+ changed += _sync_eslint_docs(root / "packages/karni-ui/eslint-plugin/lib/docs.js", dry_run)
137
+ return changed
138
+
139
+
140
+ # Prose (Markdown, YAML, HTML, templates) links to the site and repository by
141
+ # URL. A Karni-looking URL that is not the configured one is stale.
142
+ _PROSE_GLOBS = (
143
+ "README.md",
144
+ "SECURITY.md",
145
+ "CONTRIBUTING.md",
146
+ "CHANGELOG.md",
147
+ "docs/**/*.md",
148
+ "docs/**/*.html",
149
+ "docs/**/*.txt",
150
+ ".github/**/*.yml",
151
+ "packages/*/README.md",
152
+ "modules/*/README.md",
153
+ "modules/*/ui/README.md",
154
+ "templates/**/*.md",
155
+ "examples/*/README.md",
156
+ )
157
+ _KARNI_URL = re.compile(
158
+ r"https://(?:[\w.-]+\.github\.io/karni/?|github\.com/[\w.-]+/karni(?:[/#.)]|$))"
159
+ )
160
+
161
+
162
+ def stale_prose(root: Path) -> list[str]:
163
+ """``file:line: url`` for every Karni site or repository URL that differs from the constants."""
164
+ stale: list[str] = []
165
+ for glob in _PROSE_GLOBS:
166
+ for path in sorted(root.glob(glob)):
167
+ if "node_modules" in path.parts or "site" in path.parts[:1]:
168
+ continue
169
+ for number, line in enumerate(path.read_text(errors="replace").splitlines(), 1):
170
+ for match in _KARNI_URL.finditer(line):
171
+ url = match.group(0).rstrip("/#.)")
172
+ if not (url.startswith(DOCS_BASE_URL.rstrip("/")) or url.startswith(REPO_URL)):
173
+ stale.append(f"{path.relative_to(root)}:{number}: {url}")
174
+ return stale
175
+
176
+
177
+ def rewrite_prose(root: Path, old: str, new: str) -> list[str]:
178
+ """Replace ``old`` with ``new`` in every prose file; returns the files that changed."""
179
+ changed: list[str] = []
180
+ for glob in _PROSE_GLOBS:
181
+ for path in sorted(root.glob(glob)):
182
+ if "node_modules" in path.parts:
183
+ continue
184
+ text = path.read_text(errors="replace")
185
+ changed += _write(path, text, text.replace(old, new), dry_run=False)
186
+ return changed
187
+
188
+
189
+ def main(argv: list[str] | None = None) -> int:
190
+ args = sys.argv[1:] if argv is None else argv
191
+ root = repo_root()
192
+ if root is None:
193
+ print("karni.links: not inside the Karni repository", file=sys.stderr)
194
+ return 2
195
+ if args == ["sync"]:
196
+ for path in sync(root):
197
+ print(f"updated {path}")
198
+ for entry in stale_prose(root):
199
+ print(f"still stale (prose; use `sync --rewrite OLD NEW`): {entry}")
200
+ return 0
201
+ if len(args) == 4 and args[:2] == ["sync", "--rewrite"]:
202
+ for path in [*sync(root), *rewrite_prose(root, args[2], args[3])]:
203
+ print(f"updated {path}")
204
+ return 0
205
+ if args == ["check"]:
206
+ stale = [f"stale: {path}" for path in sync(root, dry_run=True)] + [
207
+ f"stale: {entry}" for entry in stale_prose(root)
208
+ ]
209
+ for line in stale:
210
+ print(f"{line} (run `python -m karni.links sync`)")
211
+ return 1 if stale else 0
212
+ print("usage: python -m karni.links [check|sync|sync --rewrite OLD NEW]", file=sys.stderr)
213
+ return 2
214
+
215
+
216
+ if __name__ == "__main__":
217
+ sys.exit(main())
@@ -0,0 +1,8 @@
1
+ {
2
+ "project_name": "My Project",
3
+ "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_').replace('-', '_') }}",
4
+ "domain": "example.com",
5
+ "karni_ui": "^0.3.0",
6
+ "karni_ui_mount": "",
7
+ "karni_baseline": "0.0.0"
8
+ }
@@ -0,0 +1,7 @@
1
+ .git
2
+ .env
3
+ .venv
4
+ **/__pycache__
5
+ *.pyc
6
+ frontend/node_modules
7
+ frontend/dist
@@ -0,0 +1,85 @@
1
+ # Environment for {{ cookiecutter.project_name }}. Copy to .env:
2
+ # cp .env.example .env
3
+ # Every value has a sane dev default baked into karni; this file makes them
4
+ # explicit. Commented-out vars are optional — leave them commented rather
5
+ # than empty (an empty value overrides the karni default with "").
6
+
7
+ # --- Core --------------------------------------------------------------------
8
+ DJANGO_ENV=dev
9
+ SECRET_KEY=karni-insecure-dev-key
10
+ DEBUG=True
11
+ ALLOWED_HOSTS=*
12
+ PROJECT_NAME={{ cookiecutter.project_slug }}
13
+
14
+ # --- Services (docker-compose hostnames) -------------------------------------
15
+ DATABASE_URL=postgres://karni:karni@db:5432/karni
16
+ REDIS_URL=redis://redis:6379/0
17
+ # "redis" (at REDIS_URL) or "locmem" — an in-process cache for a host run
18
+ # without Redis (development only: throttle counters are then per process).
19
+ # KARNI_CACHE=redis
20
+
21
+ # --- API ---------------------------------------------------------------------
22
+ KARNI_PAGE_SIZE=50
23
+ THROTTLE_ANON=100/min
24
+ THROTTLE_USER=1000/min
25
+ # Comma-separated origins; dev allows all origins regardless.
26
+ # CORS_ALLOWED_ORIGINS=https://app.{{ cookiecutter.domain }}
27
+ # Proxies between clients and Django (Caddy = 1) for throttle IP resolution.
28
+ # KARNI_NUM_PROXIES=1
29
+ # Max ?expand= nesting depth on auto-generated endpoints.
30
+ # KARNI_API_MAX_EXPAND_DEPTH=2
31
+ # Swap the search backend (dotted path to a karni SearchProvider subclass).
32
+ # KARNI_SEARCH_PROVIDER=karni.api.search.postgres.PostgresSearchProvider
33
+
34
+ # --- Auth & accounts ---------------------------------------------------------
35
+ # "jwt" (SimpleJWT token pairs) or "session" (cookie auth + CSRF bootstrap).
36
+ KARNI_AUTH_MODE=jwt
37
+ # SPA origin used in account emails (verify/reset links) and, in session
38
+ # mode, added to CSRF_TRUSTED_ORIGINS — the browser's origin must match it.
39
+ KARNI_FRONTEND_URL=http://localhost:5173
40
+ # Roles (Django groups) every self-registered user starts with.
41
+ # KARNI_ACCOUNTS_DEFAULT_ROLES=attendee
42
+ # Extra `karni seed` logins (demo password) with their roles, as JSON.
43
+ # KARNI_SEED_LOGINS={"organizer@example.com": ["organizer"], "attendee@example.com": ["attendee"]}
44
+ # Social login via django-allauth (requires karni[social]; session mode only).
45
+ KARNI_SOCIAL_AUTH=False
46
+ # TOTP two-factor auth via django-otp (requires karni[2fa]).
47
+ KARNI_2FA=False
48
+ # Comma-separated allauth providers, e.g. google,github (with KARNI_SOCIAL_AUTH).
49
+ # KARNI_SOCIAL_PROVIDERS=google
50
+ # JWT lifetimes: access in minutes, refresh in days.
51
+ # KARNI_JWT_ACCESS_LIFETIME=15
52
+ # KARNI_JWT_REFRESH_LIFETIME=7
53
+ # JSON object of per-scope auth throttle overrides (null disables a scope).
54
+ # KARNI_AUTH_THROTTLE_RATES={"auth.login": "10/min"}
55
+
56
+ # --- Admin -------------------------------------------------------------------
57
+ # KARNI_ADMIN_TITLE={{ cookiecutter.project_name }} admin
58
+ # KARNI_ADMIN_AUTODISCOVER=True
59
+ # Comma-separated app labels or app_label.ModelName entries to skip.
60
+ # KARNI_ADMIN_EXCLUDE=sessions,contenttypes,admin
61
+
62
+ # --- Storage -----------------------------------------------------------------
63
+ # "local" (filesystem) or "s3" (any S3-compatible service; requires karni[s3]).
64
+ KARNI_STORAGE=local
65
+ MEDIA_ROOT=/data/media
66
+ STATIC_ROOT=/data/static
67
+ # AWS_ACCESS_KEY_ID=
68
+ # AWS_SECRET_ACCESS_KEY=
69
+ # AWS_STORAGE_BUCKET_NAME=
70
+ # AWS_S3_ENDPOINT_URL=
71
+ # AWS_S3_REGION_NAME=
72
+
73
+ # --- Observability -----------------------------------------------------------
74
+ # Sentry is disabled while SENTRY_DSN is empty (requires karni[sentry]).
75
+ SENTRY_DSN=
76
+ SENTRY_TRACES_SAMPLE_RATE=0.0
77
+
78
+ # --- Deployment --------------------------------------------------------------
79
+ # Domain Caddy serves (and provisions TLS for) in production.
80
+ DOMAIN={{ cookiecutter.domain }}
81
+
82
+ # Path *inside this project* to a local karni checkout, installed at image
83
+ # build time instead of the PyPI release (see README.md). Example:
84
+ # cp -r /path/to/Karni/packages/karni ./.karni-src then KARNI_SRC=.karni-src
85
+ KARNI_SRC=
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .env
5
+ .karni-src/
6
+ frontend/node_modules/
7
+ frontend/dist/
8
+ /data/
9
+ # Machine-local karni state (one-time tips etc.); also self-ignored via its own .gitignore.
10
+ .karni/local/
@@ -0,0 +1,22 @@
1
+ # Caddy front for {{ cookiecutter.project_name }} in production.
2
+ #
3
+ # Caddy provisions TLS automatically for the DOMAIN env var (defaults to
4
+ # localhost, which uses a self-signed local CA). Static and media files are
5
+ # served straight from /data — share the /data volume with the django
6
+ # container (STATIC_ROOT=/data/static, MEDIA_ROOT=/data/media).
7
+
8
+ {$DOMAIN:localhost} {
9
+ encode gzip
10
+
11
+ handle /static/* {
12
+ root * /data
13
+ file_server
14
+ }
15
+
16
+ handle /media/* {
17
+ root * /data
18
+ file_server
19
+ }
20
+
21
+ reverse_proxy django:8000
22
+ }
@@ -0,0 +1,53 @@
1
+ # syntax=docker/dockerfile:1
2
+ # Multi-stage build for {{ cookiecutter.project_name }}.
3
+ #
4
+ # Targets:
5
+ # dev — used by docker-compose; runserver-friendly, source bind-mounted over /app.
6
+ # prod — fresh slim image, non-root user, gunicorn with uvicorn workers.
7
+ #
8
+ # Build arg KARNI_SRC: path *inside this project* to a local karni checkout,
9
+ # installed before (and therefore instead of) the PyPI release. Until karni is
10
+ # published, vendor it and set KARNI_SRC in .env — see README.md.
11
+
12
+ FROM python:3.12-slim AS base
13
+ ENV PYTHONDONTWRITEBYTECODE=1 \
14
+ PYTHONUNBUFFERED=1
15
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
16
+
17
+ FROM base AS deps
18
+ WORKDIR /app
19
+ RUN uv venv /venv
20
+ ARG KARNI_SRC=""
21
+ # Copy the whole context so a vendored karni checkout (KARNI_SRC) is available.
22
+ COPY . /app/
23
+ # Installing KARNI_SRC first satisfies the "karni" requirement, so the
24
+ # pyproject install below skips resolving it from PyPI.
25
+ RUN if [ -n "${KARNI_SRC}" ]; then \
26
+ uv pip install --python /venv/bin/python "./${KARNI_SRC}"; \
27
+ fi \
28
+ && uv pip install --python /venv/bin/python -r pyproject.toml
29
+
30
+ FROM base AS dev
31
+ WORKDIR /app
32
+ ENV PATH="/venv/bin:${PATH}"
33
+ COPY --from=deps /venv /venv
34
+ COPY . /app/
35
+ EXPOSE 8000
36
+ CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
37
+
38
+ FROM python:3.12-slim AS prod
39
+ WORKDIR /app
40
+ ENV PYTHONDONTWRITEBYTECODE=1 \
41
+ PYTHONUNBUFFERED=1 \
42
+ PATH="/venv/bin:${PATH}"
43
+ RUN addgroup --system app && adduser --system --ingroup app app
44
+ COPY --from=deps /venv /venv
45
+ COPY . /app/
46
+ # collectstatic at build time. DJANGO_ENV=dev + a dummy SECRET_KEY keep the
47
+ # strict prod validation out of a command that touches no real config.
48
+ RUN mkdir -p /data/static /data/media \
49
+ && DJANGO_ENV=dev SECRET_KEY=build-only-dummy python manage.py collectstatic --noinput \
50
+ && chown -R app:app /data
51
+ USER app
52
+ EXPOSE 8000
53
+ CMD ["gunicorn", "config.asgi:application", "-k", "uvicorn_worker.UvicornWorker", "-b", "0.0.0.0:8000"]
@@ -0,0 +1,52 @@
1
+ .PHONY: up down serve migrate types check context rules agents test shell routes schedules superuser logs
2
+
3
+ # Every target is a thin alias for a `karni` command, which runs through the
4
+ # project .venv when present, else the running compose service.
5
+
6
+ up:
7
+ karni serve -d
8
+
9
+ down:
10
+ docker compose down
11
+
12
+ serve:
13
+ karni serve
14
+
15
+ migrate:
16
+ karni make migration
17
+ karni migrate
18
+
19
+ types:
20
+ karni types
21
+
22
+ check:
23
+ karni check
24
+ karni rules sync --check
25
+
26
+ # Agent guidance: conventions, and regenerating AGENTS.md / CLAUDE.md / tool adapters.
27
+ rules:
28
+ karni rules
29
+
30
+ agents:
31
+ karni rules sync
32
+
33
+ context:
34
+ karni context
35
+
36
+ test:
37
+ karni test
38
+
39
+ shell:
40
+ karni shell
41
+
42
+ routes:
43
+ karni routes
44
+
45
+ schedules:
46
+ karni schedule list
47
+
48
+ superuser:
49
+ karni createsuperuser
50
+
51
+ logs:
52
+ docker compose logs -f django worker beat
@@ -0,0 +1,169 @@
1
+ # {{ cookiecutter.project_name }}
2
+
3
+ A [Karni](https://github.com/karni) project: batteries-included Django + DRF +
4
+ Celery, configured almost entirely by `karni_settings(globals())` in
5
+ `config/settings.py`. Every Karni default can be overridden from that file —
6
+ anything you assign after the call wins.
7
+
8
+ ## Quickstart
9
+
10
+ ```sh
11
+ cp .env.example .env
12
+ docker compose up --build
13
+ ```
14
+
15
+ This boots:
16
+
17
+ | Service | What | Where |
18
+ | ---------- | ----------------------------------------------- | ---------------------- |
19
+ | `django` | migrations, then Django dev server | http://localhost:8000 |
20
+ | `db` | postgres:16 | internal (`db:5432`) |
21
+ | `redis` | redis:7 (Celery broker + cache) | internal (`redis:6379`)|
22
+ | `worker` | Celery worker | — |
23
+ | `beat` | Celery beat (django-celery-beat DB scheduler) | — |
24
+ | `frontend` | Vite dev server (React + TS), proxies `/api` and `/health` to django | http://localhost:5173 |
25
+
26
+ Verify the stack (Postgres, Redis and Celery all checked):
27
+
28
+ ```sh
29
+ curl http://localhost:8000/health/ready/
30
+ # {"status": "ok", "checks": {"database": ..., "cache": ..., "celery": ...}}
31
+ ```
32
+
33
+ Other endpoints: liveness at `/health/`, admin at `/admin/`, OpenAPI schema at
34
+ `/api/schema/`, Swagger UI at `/api/docs/`.
35
+
36
+ ## Frontend
37
+
38
+ `frontend/` is Vite + React + TypeScript on [@karni/ui](https://github.com/karni):
39
+ `src/App.tsx` mounts `KarniProvider` (one `createKarniClient({ baseUrl: "/api" })`),
40
+ `AuthProvider`, the router and a `Toaster`; `src/main.tsx` imports
41
+ `@karni/ui/styles.css` (the `--karni-*` design tokens) and `src/index.css`
42
+ (Tailwind v3 with the `@karni/ui/tailwind-preset`, see `tailwind.config.ts`).
43
+ Every route renders inside `src/layout.tsx` — @karni/ui's `Layout` with one
44
+ nav entry per static route in `src/routes.tsx`, so pages from
45
+ `karni make page` (or `karni make model ... -a`) appear in the sidebar.
46
+
47
+ ```sh
48
+ cd frontend
49
+ npm install
50
+ npm run dev # http://localhost:5173 — proxies /api and /health to Django
51
+ npm run build # tsc + vite build into frontend/dist
52
+ npm run lint # ESLint with the karni/* enforcement rules
53
+ ```
54
+
55
+ The dev server proxies to `http://127.0.0.1:8000` (`karni serve --local`);
56
+ docker compose sets `KARNI_API_PROXY=http://django:8000` for its container.
57
+
58
+ ## Using a local Karni checkout
59
+
60
+ Until karni and @karni/ui are published, a project can run against a Karni
61
+ checkout.
62
+
63
+ **@karni/ui.** `karni new` resolves it like the project template:
64
+ `KARNI_UI_SRC=/path/to/Karni/packages/karni-ui` > the Karni checkout `karni`
65
+ itself runs from > the npm release (`^0.1.0`). A local source is written into
66
+ `frontend/package.json` as `"@karni/ui": "file:/abs/path/to/packages/karni-ui"`,
67
+ and `frontend/.npmrc` (`install-links=true`) installs it as a packed copy —
68
+ exactly like the npm release, so React and TanStack Query exist once. The copy
69
+ ships the checkout's built `dist/` (`karni new` builds it when missing). The
70
+ copy is a snapshot — a plain `npm install` does not notice a rebuilt checkout.
71
+ After changing @karni/ui, rebuild it and refresh the copy:
72
+
73
+ ```sh
74
+ (cd /path/to/Karni/packages/karni-ui && npm run build)
75
+ (cd frontend && rm -rf node_modules/@karni/ui && npm install)
76
+ # docker compose: docker compose exec frontend rm -rf node_modules/@karni/ui
77
+ # docker compose restart frontend
78
+ ```
79
+
80
+ Under docker compose the `frontend` container runs `npm install` on start and
81
+ gets the checkout mounted read-only at the same absolute path, so the `file:`
82
+ spec resolves inside the container too (the mount is in `docker-compose.yml`
83
+ when `karni new` found a local @karni/ui). The container ignores
84
+ `package-lock.json`, which records the checkout relative to the host's
85
+ `frontend/`. Moving the checkout means updating both `frontend/package.json`
86
+ and that mount, or running `karni new` again.
87
+
88
+ Without Docker, run everything on the host. Install the project's own
89
+ dependencies into the venv first (`pip install -e . pytest pytest-django`:
90
+ that is what brings `psycopg` for a host PostgreSQL, and pytest for
91
+ `karni test`). Point `.env` at local services
92
+ (`DATABASE_URL=sqlite:///db.sqlite3`, `REDIS_URL=redis://127.0.0.1:6379/0`,
93
+ or `KARNI_CACHE=locmem` to run without Redis at all); settings.py loads it
94
+ automatically (variables you already exported win; set `KARNI_DOTENV=false`
95
+ to skip it). Then `karni serve --local` for Django and `npm run dev` in
96
+ `frontend/`; `karni worker` only when something needs Celery:
97
+
98
+ ```sh
99
+ karni migrate && karni serve --local # http://127.0.0.1:8000
100
+ (cd frontend && npm run dev) # http://localhost:5173, in a second shell
101
+ ```
102
+
103
+ **karni (Python).** For the host `.venv` (`karni serve --local`,
104
+ `karni migrate`, ...), install the checkout next to the project's other
105
+ dependencies:
106
+
107
+ ```sh
108
+ uv venv && uv pip install -e /path/to/Karni/packages/karni -r pyproject.toml
109
+ ```
110
+
111
+ For Docker, vendor a checkout into the project and point the `KARNI_SRC`
112
+ build arg at it (docker-compose reads it from `.env`):
113
+
114
+ ```sh
115
+ cp -r /path/to/Karni/packages/karni ./.karni-src
116
+ echo "KARNI_SRC=.karni-src" >> .env # or edit the existing line
117
+ docker compose build
118
+ ```
119
+
120
+ `KARNI_SRC` must be a path inside this project (the Docker build context).
121
+ The vendored copy is installed before the PyPI dependencies, so it takes
122
+ precedence.
123
+
124
+ ## Day-to-day
125
+
126
+ Generate code with `karni make` (`karni make --list` shows every generator;
127
+ each runs through the project's `.venv` or the running compose service where
128
+ it needs Django):
129
+
130
+ ```sh
131
+ karni make app events # replaces manage.py startapp; wires INSTALLED_APPS
132
+ karni make model Event title:str:200 starts_at:datetime organizer:fk:User -a \
133
+ --access owned:organizer # model + viewset + list/detail/form pages
134
+ karni make migration && karni migrate
135
+ karni createsuperuser
136
+ ```
137
+
138
+ Override any Django, DRF, Celery or Karni setting in `config/settings.py`,
139
+ below the `karni_settings()` call. Generators add apps to the `# karni:apps`
140
+ block and middleware to the `# karni:middleware` block — keep those markers.
141
+
142
+ ## AI coding agents
143
+
144
+ `karni new` generated thin agent adapters — `AGENTS.md`, `CLAUDE.md`,
145
+ `.cursor/rules/karni.mdc`, `.windsurf/rules/karni.md`,
146
+ `.github/copilot-instructions.md`, `.clinerules/karni.md`, `CONVENTIONS.md`,
147
+ `.junie/AGENTS.md` and slash commands. They all say the same thing: run
148
+ `karni context` and `karni rules` first. Do not edit them; put project rules
149
+ in `karni.rules.py` and run `karni rules sync` (`make check` fails when they
150
+ are stale). `karni mcp` serves the same surface over MCP.
151
+
152
+ ## Production
153
+
154
+ The `prod` Dockerfile target builds a slim, non-root image that runs
155
+ `gunicorn config.asgi:application` with uvicorn workers and collects static
156
+ files to `/data/static` at build time:
157
+
158
+ ```sh
159
+ docker build --target prod -t {{ cookiecutter.project_slug }}:latest .
160
+ ```
161
+
162
+ Run it with `DJANGO_ENV=prod`, a real `SECRET_KEY` and `ALLOWED_HOSTS` —
163
+ Karni validates the configuration at startup and fails loudly on anything
164
+ insecure or missing. Put Caddy in front using the provided `Caddyfile`: it
165
+ provisions TLS for `$DOMAIN` automatically, serves `/static/*` and `/media/*`
166
+ from `/data` (share the `/data` volume between the django container and
167
+ Caddy), and reverse-proxies everything else to `django:8000`. For the
168
+ frontend, `npm run build` in `frontend/` produces static assets you can serve
169
+ from Caddy or any CDN.
@@ -0,0 +1,16 @@
1
+ """Project package for {{ cookiecutter.project_name }}.
2
+
3
+ Runs first for every entry point (manage.py, ASGI/WSGI, ``celery -A config``):
4
+ it loads ``.env`` for host runs (variables already set, e.g. by Docker Compose
5
+ or CI, always win; ``KARNI_DOTENV=false`` skips it), then imports the Celery
6
+ app so ``@shared_task`` binds to it whenever Django starts
7
+ (see https://docs.celeryq.dev/en/stable/django/).
8
+ """
9
+
10
+ from karni.dotenv import load_dotenv
11
+
12
+ load_dotenv(__file__)
13
+
14
+ from config.celery import app as celery_app # noqa: E402
15
+
16
+ __all__ = ["celery_app"]