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.
- karni_cli/__init__.py +3 -0
- karni_cli/_agent/__init__.py +82 -0
- karni_cli/_agent/__main__.py +44 -0
- karni_cli/_agent/check.py +658 -0
- karni_cli/_agent/history.py +328 -0
- karni_cli/_agent/history_chronicle.py +246 -0
- karni_cli/_agent/overrides.py +194 -0
- karni_cli/_agent/rules.py +673 -0
- karni_cli/_changelog/0.2.0.toml +304 -0
- karni_cli/_changelog/0.3.0.toml +25 -0
- karni_cli/_changelog/__init__.py +248 -0
- karni_cli/_errorcodes/__init__.py +1171 -0
- karni_cli/_errorcodes/__main__.py +34 -0
- karni_cli/_errorcodes/links.py +217 -0
- karni_cli/_templates/project/cookiecutter.json +8 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/.dockerignore +7 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/.env.example +85 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/.gitignore +10 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/Caddyfile +22 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/Makefile +52 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/README.md +169 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/__init__.py +16 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/asgi.py +14 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/celery.py +17 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/settings.py +28 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/urls.py +31 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/wsgi.py +14 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +126 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/.dockerignore +2 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/.jscpd.json +9 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/.npmrc +7 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/Dockerfile.dev +17 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/eslint.config.js +32 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/index.html +14 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/package.json +43 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/postcss.config.js +6 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/public/favicon.svg +6 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/App.tsx +18 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/api/health.ts +21 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/index.css +11 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/layout.tsx +51 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/main.tsx +11 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/pages/home.tsx +28 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/routes.tsx +24 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/vite-env.d.ts +1 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/tailwind.config.ts +13 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/tsconfig.json +19 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/vite.config.ts +18 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/manage.py +23 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/pyproject.toml +31 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/tests/__init__.py +0 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/tests/test_api_schema.py +17 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/tests/test_smoke.py +11 -0
- karni_cli/adapters.py +373 -0
- karni_cli/agent_adapters.toml +160 -0
- karni_cli/agent_data.py +260 -0
- karni_cli/brand.py +55 -0
- karni_cli/clidoc.py +254 -0
- karni_cli/commands/__init__.py +0 -0
- karni_cli/commands/add.py +144 -0
- karni_cli/commands/beat.py +36 -0
- karni_cli/commands/check.py +101 -0
- karni_cli/commands/config.py +81 -0
- karni_cli/commands/context.py +171 -0
- karni_cli/commands/createsuperuser.py +28 -0
- karni_cli/commands/doctor.py +136 -0
- karni_cli/commands/explain.py +60 -0
- karni_cli/commands/make.py +55 -0
- karni_cli/commands/manage_passthrough.py +28 -0
- karni_cli/commands/mcp.py +20 -0
- karni_cli/commands/migrate.py +23 -0
- karni_cli/commands/new.py +106 -0
- karni_cli/commands/routes.py +20 -0
- karni_cli/commands/rules.py +139 -0
- karni_cli/commands/run_tests.py +38 -0
- karni_cli/commands/schedule.py +40 -0
- karni_cli/commands/seed.py +34 -0
- karni_cli/commands/serve.py +50 -0
- karni_cli/commands/shell.py +28 -0
- karni_cli/commands/sync_roles.py +32 -0
- karni_cli/commands/types.py +72 -0
- karni_cli/commands/upgrade.py +231 -0
- karni_cli/commands/worker.py +63 -0
- karni_cli/config.py +142 -0
- karni_cli/environment.py +29 -0
- karni_cli/errorcodes.py +377 -0
- karni_cli/errordoc.py +227 -0
- karni_cli/generators/__init__.py +67 -0
- karni_cli/generators/apitest.py +206 -0
- karni_cli/generators/app.py +103 -0
- karni_cli/generators/command.py +98 -0
- karni_cli/generators/component.py +115 -0
- karni_cli/generators/core.py +319 -0
- karni_cli/generators/event.py +57 -0
- karni_cli/generators/factory.py +88 -0
- karni_cli/generators/fields.py +343 -0
- karni_cli/generators/introspect.py +263 -0
- karni_cli/generators/listener.py +168 -0
- karni_cli/generators/middleware.py +95 -0
- karni_cli/generators/migration.py +82 -0
- karni_cli/generators/model.py +235 -0
- karni_cli/generators/module.py +255 -0
- karni_cli/generators/naming.py +100 -0
- karni_cli/generators/page.py +373 -0
- karni_cli/generators/pysource.py +304 -0
- karni_cli/generators/schedule.py +174 -0
- karni_cli/generators/seeder.py +86 -0
- karni_cli/generators/serializer.py +165 -0
- karni_cli/generators/service.py +68 -0
- karni_cli/generators/task.py +84 -0
- karni_cli/generators/viewset.py +179 -0
- karni_cli/history.py +59 -0
- karni_cli/local_state.py +73 -0
- karni_cli/main.py +104 -0
- karni_cli/mcp_server.py +103 -0
- karni_cli/project.py +248 -0
- karni_cli/rules_render.py +86 -0
- karni_cli/scaffold.py +197 -0
- karni_cli/settings_edit.py +128 -0
- karni_cli/tips.py +53 -0
- karni_cli/typegen.py +101 -0
- karni_cli/upgrades/__init__.py +28 -0
- karni_cli/upgrades/changelog.py +123 -0
- karni_cli/upgrades/codemod.py +125 -0
- karni_cli/upgrades/engine.py +224 -0
- karni_cli/upgrades/legacy_agent_files.py +138 -0
- karni_cli/upgrades/python.py +670 -0
- karni_cli/upgrades/transforms/rename-export.cjs +96 -0
- karni_cli/upgrades/typescript.py +160 -0
- karni_cli/upgrades/v0_2/__init__.py +1 -0
- karni_cli/upgrades/v0_2/agent_guidance.py +269 -0
- karni_cli/upgrades/v1_0/__init__.py +5 -0
- karni_cli/upgrades/v1_0/all_objects_to_without.py +18 -0
- karni_cli/upgrades/v1_0/all_with_deleted_to_without.py +13 -0
- karni_cli/upgrades/v1_0/deleted_only_to_without_dead.py +13 -0
- karni_cli/upgrades/v1_0/object_permission_classes_to_access.py +16 -0
- karni_cli/upgrades/v1_0/related_lookups_to_optimize.py +15 -0
- karni_cli/upgrades/v1_0/scoped_related_lookups_to_optimize.py +15 -0
- karni_cli/upgrades/v1_0/soft_delete_manager_mixin_to_scoped.py +14 -0
- karni_cli-0.3.1.dist-info/METADATA +191 -0
- karni_cli-0.3.1.dist-info/RECORD +145 -0
- karni_cli-0.3.1.dist-info/WHEEL +4 -0
- karni_cli-0.3.1.dist-info/entry_points.txt +2 -0
- 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,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,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"]
|