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,194 @@
|
|
|
1
|
+
"""Project overrides: ``karni.rules.py`` at the project root.
|
|
2
|
+
|
|
3
|
+
The file is read **statically** — parsed with :mod:`ast`, every value
|
|
4
|
+
evaluated with :func:`ast.literal_eval` — and never imported or executed, so
|
|
5
|
+
the CLI and the MCP server can honour it without running project code.
|
|
6
|
+
Schema (every name optional)::
|
|
7
|
+
|
|
8
|
+
# karni.rules.py
|
|
9
|
+
ADD = [ # project rules; codes are yours (any prefix except K), e.g. P001
|
|
10
|
+
{"code": "P001", "rule": "Money is always a Decimal, never a float.",
|
|
11
|
+
"summary": "Money is Decimal, never float.", # optional (defaults to rule)
|
|
12
|
+
"category": "project", "severity": "must", # optional
|
|
13
|
+
"rationale": "...", "fix": "..."}, # optional
|
|
14
|
+
]
|
|
15
|
+
OVERRIDE = {"K202": {"summary": "Page files stay at 80 lines or fewer."}}
|
|
16
|
+
DISABLE = ["K304"]
|
|
17
|
+
ADAPTERS = {"disable": ["copilot"], "paths": {"windsurf": ".devin/rules/karni.md"}}
|
|
18
|
+
|
|
19
|
+
``ADD += [...]`` and ``DISABLE += [...]`` are accepted (the upgrade codemod
|
|
20
|
+
appends that way). Anything else at top level is an error that names the line.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import ast
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass, field, replace
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from .rules import CATEGORIES, RULES, SEVERITIES, Rule
|
|
32
|
+
|
|
33
|
+
OVERRIDE_FILE = "karni.rules.py"
|
|
34
|
+
|
|
35
|
+
_NAMES = ("ADD", "OVERRIDE", "DISABLE", "ADAPTERS")
|
|
36
|
+
_EXTENDABLE = ("ADD", "DISABLE")
|
|
37
|
+
#: Fields a project may change on a Karni rule. ``code`` / ``enforced_by`` are fixed.
|
|
38
|
+
OVERRIDABLE_FIELDS = frozenset({"summary", "rule", "rationale", "fix", "severity", "category"})
|
|
39
|
+
_ADD_FIELDS = OVERRIDABLE_FIELDS | {"code"}
|
|
40
|
+
#: Project codes: 1-3 capitals (never a bare K…, which is Karni's) + 3 digits.
|
|
41
|
+
PROJECT_CODE_RE = re.compile(r"^(?!K\d)[A-Z]{1,3}\d{3}$")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class OverrideError(ValueError):
|
|
45
|
+
"""``karni.rules.py`` is malformed; the message says where and why (code ``E701``)."""
|
|
46
|
+
|
|
47
|
+
code = "E701"
|
|
48
|
+
|
|
49
|
+
def __str__(self) -> str:
|
|
50
|
+
return f"{self.code}: {super().__str__()} (karni explain {self.code})"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ProjectOverrides:
|
|
55
|
+
path: Path | None = None
|
|
56
|
+
add: list[dict[str, Any]] = field(default_factory=list)
|
|
57
|
+
override: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
58
|
+
disable: list[str] = field(default_factory=list)
|
|
59
|
+
adapters: dict[str, Any] = field(default_factory=dict)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _literal(node: ast.expr, name: str, path: Path) -> Any:
|
|
63
|
+
try:
|
|
64
|
+
return ast.literal_eval(node)
|
|
65
|
+
except ValueError:
|
|
66
|
+
raise OverrideError(
|
|
67
|
+
f"{path}:{node.lineno}: {name} must be a literal (strings, lists, dicts) — "
|
|
68
|
+
"karni.rules.py is read statically and never executed."
|
|
69
|
+
) from None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def parse_overrides(source: str, path: Path) -> ProjectOverrides:
|
|
73
|
+
try:
|
|
74
|
+
tree = ast.parse(source, filename=str(path))
|
|
75
|
+
except SyntaxError as exc:
|
|
76
|
+
raise OverrideError(f"{path}:{exc.lineno}: syntax error: {exc.msg}") from None
|
|
77
|
+
values: dict[str, Any] = {}
|
|
78
|
+
for stmt in tree.body:
|
|
79
|
+
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant):
|
|
80
|
+
continue # docstrings / bare strings
|
|
81
|
+
if (
|
|
82
|
+
isinstance(stmt, ast.Assign)
|
|
83
|
+
and len(stmt.targets) == 1
|
|
84
|
+
and isinstance(stmt.targets[0], ast.Name)
|
|
85
|
+
and stmt.targets[0].id in _NAMES
|
|
86
|
+
):
|
|
87
|
+
name = stmt.targets[0].id
|
|
88
|
+
values[name] = _literal(stmt.value, name, path)
|
|
89
|
+
continue
|
|
90
|
+
if (
|
|
91
|
+
isinstance(stmt, ast.AugAssign)
|
|
92
|
+
and isinstance(stmt.op, ast.Add)
|
|
93
|
+
and isinstance(stmt.target, ast.Name)
|
|
94
|
+
and stmt.target.id in _EXTENDABLE
|
|
95
|
+
):
|
|
96
|
+
name = stmt.target.id
|
|
97
|
+
extra = _literal(stmt.value, name, path)
|
|
98
|
+
if not isinstance(extra, list):
|
|
99
|
+
raise OverrideError(f"{path}:{stmt.lineno}: {name} += expects a list.")
|
|
100
|
+
values[name] = [*values.get(name, []), *extra]
|
|
101
|
+
continue
|
|
102
|
+
raise OverrideError(
|
|
103
|
+
f"{path}:{stmt.lineno}: only literal assignments to {', '.join(_NAMES)} "
|
|
104
|
+
"(and ADD/DISABLE +=) are allowed in karni.rules.py."
|
|
105
|
+
)
|
|
106
|
+
return _validate(values, path)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _validate(values: dict[str, Any], path: Path) -> ProjectOverrides:
|
|
110
|
+
known = {rule.code for rule in RULES}
|
|
111
|
+
add = values.get("ADD", [])
|
|
112
|
+
override = values.get("OVERRIDE", {})
|
|
113
|
+
disable = values.get("DISABLE", [])
|
|
114
|
+
adapters = values.get("ADAPTERS", {})
|
|
115
|
+
if not isinstance(add, list) or not all(isinstance(e, dict) for e in add):
|
|
116
|
+
raise OverrideError(f"{path}: ADD must be a list of dicts.")
|
|
117
|
+
if not isinstance(override, dict) or not all(isinstance(v, dict) for v in override.values()):
|
|
118
|
+
raise OverrideError(f"{path}: OVERRIDE must map codes to dicts of fields.")
|
|
119
|
+
if not isinstance(disable, list) or not all(isinstance(c, str) for c in disable):
|
|
120
|
+
raise OverrideError(f"{path}: DISABLE must be a list of codes.")
|
|
121
|
+
if not isinstance(adapters, dict) or set(adapters) - {"disable", "paths"}:
|
|
122
|
+
raise OverrideError(f'{path}: ADAPTERS accepts only "disable" and "paths".')
|
|
123
|
+
|
|
124
|
+
seen: set[str] = set()
|
|
125
|
+
for entry in add:
|
|
126
|
+
code = entry.get("code")
|
|
127
|
+
if not isinstance(code, str) or not PROJECT_CODE_RE.match(code):
|
|
128
|
+
raise OverrideError(
|
|
129
|
+
f"{path}: ADD code {code!r} must look like P001 (K codes are Karni's)."
|
|
130
|
+
)
|
|
131
|
+
if code in seen:
|
|
132
|
+
raise OverrideError(f"{path}: ADD code {code} is defined twice.")
|
|
133
|
+
seen.add(code)
|
|
134
|
+
if not isinstance(entry.get("rule"), str) or not entry["rule"].strip():
|
|
135
|
+
raise OverrideError(f"{path}: ADD {code} needs a non-empty 'rule'.")
|
|
136
|
+
_check_fields(entry, _ADD_FIELDS, f"ADD {code}", path)
|
|
137
|
+
for code, fields in override.items():
|
|
138
|
+
if code not in known:
|
|
139
|
+
raise OverrideError(f"{path}: OVERRIDE {code} is not a Karni rule code.")
|
|
140
|
+
_check_fields(fields, OVERRIDABLE_FIELDS, f"OVERRIDE {code}", path)
|
|
141
|
+
for code in disable:
|
|
142
|
+
if code not in known:
|
|
143
|
+
raise OverrideError(f"{path}: DISABLE {code} is not a Karni rule code.")
|
|
144
|
+
return ProjectOverrides(
|
|
145
|
+
path=path, add=add, override=override, disable=disable, adapters=adapters
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _check_fields(entry: dict[str, Any], allowed: frozenset[str], where: str, path: Path) -> None:
|
|
150
|
+
unknown = set(entry) - allowed
|
|
151
|
+
if unknown:
|
|
152
|
+
raise OverrideError(f"{path}: {where} has unknown field(s): {', '.join(sorted(unknown))}.")
|
|
153
|
+
for key, value in entry.items():
|
|
154
|
+
if not isinstance(value, str):
|
|
155
|
+
raise OverrideError(f"{path}: {where}.{key} must be a string.")
|
|
156
|
+
if "severity" in entry and entry["severity"] not in SEVERITIES:
|
|
157
|
+
raise OverrideError(f"{path}: {where}.severity must be one of {', '.join(SEVERITIES)}.")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def read_overrides(root: Path | str | None) -> ProjectOverrides:
|
|
161
|
+
"""Overrides from ``<root>/karni.rules.py``; empty when there is no file."""
|
|
162
|
+
if root is None:
|
|
163
|
+
return ProjectOverrides()
|
|
164
|
+
path = Path(root) / OVERRIDE_FILE
|
|
165
|
+
if not path.is_file():
|
|
166
|
+
return ProjectOverrides()
|
|
167
|
+
return parse_overrides(path.read_text(encoding="utf-8"), path)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def resolve_rules(overrides: ProjectOverrides) -> list[Rule]:
|
|
171
|
+
"""Karni's rules with the project's DISABLE / OVERRIDE / ADD applied, in render order."""
|
|
172
|
+
rules: list[Rule] = []
|
|
173
|
+
for rule in RULES:
|
|
174
|
+
if rule.code in overrides.disable:
|
|
175
|
+
continue
|
|
176
|
+
changes = overrides.override.get(rule.code)
|
|
177
|
+
rules.append(replace(rule, **changes) if changes else rule)
|
|
178
|
+
for entry in overrides.add:
|
|
179
|
+
rules.append(
|
|
180
|
+
Rule(
|
|
181
|
+
code=entry["code"],
|
|
182
|
+
category=entry.get("category", "project"),
|
|
183
|
+
severity=entry.get("severity", "must"),
|
|
184
|
+
summary=entry.get("summary", entry["rule"]),
|
|
185
|
+
rule=entry["rule"],
|
|
186
|
+
rationale=entry.get("rationale", ""),
|
|
187
|
+
fix=entry.get("fix", ""),
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
order = {name: i for i, name in enumerate(CATEGORIES)}
|
|
191
|
+
extra = [r.category for r in rules if r.category not in order]
|
|
192
|
+
for name in dict.fromkeys(extra):
|
|
193
|
+
order[name] = len(order)
|
|
194
|
+
return sorted(rules, key=lambda r: (order[r.category], r.code))
|