Track the editor config, local Claude permissions and the maintenance scripts
.editorconfig fixes indentation and line endings for every editor. The scripts/ helpers (database import checks, the monolith guard, and the two refactor migrations) were only ever local.
This commit is contained in:
parent
f996336afb
commit
b8277d6351
20
.claude/settings.local.json
Normal file
20
.claude/settings.local.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python *)",
|
||||
"Bash(DEVPLACE_DISABLE_SERVICES=1 python -)",
|
||||
"Bash(command -v hawk)",
|
||||
"Bash(export DEVPLACE_DISABLE_SERVICES=1)",
|
||||
"Bash(export DEVPLACE_DATABASE_URL=\"sqlite:///tmp/devplace_verify.db\")",
|
||||
"Bash(rm -f /tmp/devplace_verify.db)",
|
||||
"Bash(export DEVPLACE_DATABASE_URL=\"sqlite:////tmp/devplace_verify.db\")",
|
||||
"Bash",
|
||||
"Edit(/home/retoor/projects/devplacepy/devplacepy/routers/projects/containers/instances.py)",
|
||||
"Edit(/home/retoor/projects/devplacepy/devplacepy/static/js/components/ContainerTerminal.js)",
|
||||
"Edit(/home/retoor/projects/devplacepy/devplacepy/services/containers/store.py)",
|
||||
"Verify",
|
||||
"Edit(/home/retoor/projects/devplacepy/devplacepy/static/js/MessagesLayout.js)",
|
||||
"Write(/home/retoor/projects/devplacepy/devplacepy/static/css/messages.css)"
|
||||
]
|
||||
}
|
||||
}
|
||||
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
65
scripts/check_database_imports.py
Normal file
65
scripts/check_database_imports.py
Normal file
@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
ALLOW_PREFIXES = (
|
||||
ROOT / "devplacepy" / "database",
|
||||
ROOT / "devplacepy" / "db_client.py",
|
||||
ROOT / "devplacepy_services" / "database",
|
||||
ROOT / "scripts" / "migrate_db_imports.py",
|
||||
ROOT / "scripts" / "check_database_imports.py",
|
||||
)
|
||||
|
||||
PATTERNS = [
|
||||
re.compile(r"from devplacepy\.database(?:\.[a-z_]+)? import"),
|
||||
re.compile(r"import devplacepy\.database\b"),
|
||||
]
|
||||
|
||||
# Only flagged outside tests/: unit tests may legitimately use `local_db` /
|
||||
# `from devplacepy import database` directly (Section 13.4). Source code must
|
||||
# always go through `devplacepy.db_client` so the remote broker patch applies.
|
||||
SOURCE_ONLY_PATTERNS = [
|
||||
re.compile(r"from devplacepy import(?:\s+\w+,)*\s+database\b"),
|
||||
]
|
||||
|
||||
|
||||
def allowed(path: Path) -> bool:
|
||||
for prefix in ALLOW_PREFIXES:
|
||||
if path == prefix or str(path).startswith(str(prefix) + "/"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
for pattern in ("devplacepy/**/*.py", "devplacepy_services/**/*.py", "tests/**/*.py"):
|
||||
is_source = not pattern.startswith("tests/")
|
||||
for path in ROOT.glob(pattern):
|
||||
if not path.is_file() or allowed(path):
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
active_patterns = PATTERNS + SOURCE_ONLY_PATTERNS if is_source else PATTERNS
|
||||
for lineno, line in enumerate(text.splitlines(), 1):
|
||||
for rx in active_patterns:
|
||||
if rx.search(line):
|
||||
errors.append(f"{path.relative_to(ROOT)}:{lineno}: {line.strip()}")
|
||||
break
|
||||
if errors:
|
||||
print("FORBIDDEN database imports (use devplacepy.db_client):")
|
||||
for line in sorted(errors)[:80]:
|
||||
print(f" {line}")
|
||||
if len(errors) > 80:
|
||||
print(f" ... and {len(errors) - 80} more")
|
||||
return 1
|
||||
print("check_database_imports: ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
85
scripts/check_no_monolith.py
Normal file
85
scripts/check_no_monolith.py
Normal file
@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ALLOWLIST = {
|
||||
ROOT / "splitup.md",
|
||||
ROOT / "scripts/check_no_monolith.py",
|
||||
}
|
||||
|
||||
PATTERNS = [
|
||||
(r"uvicorn\s+devplacepy\.main:app", "uvicorn monolith boot"),
|
||||
(r"from devplacepy\.main import", "main.py import"),
|
||||
(r"import devplacepy\.main", "main.py import"),
|
||||
(r"devplacepy\.main:app", "main app string"),
|
||||
(r"DEVPLACE_DB_MODE", "hybrid DB mode flag"),
|
||||
(r"make dev-monolith", "dev-monolith target"),
|
||||
(r"make prod-monolith", "prod-monolith target"),
|
||||
(r"DEVPLACE_TEST_PORT=10501", "legacy test port default"),
|
||||
(r"monolith-dev", "monolith port profile"),
|
||||
]
|
||||
|
||||
PHASE = sys.argv[1] if len(sys.argv) > 1 else "m7"
|
||||
|
||||
|
||||
def scan_file(path: Path) -> list[str]:
|
||||
if path in ALLOWLIST:
|
||||
return []
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
return []
|
||||
hits = []
|
||||
for pattern, label in PATTERNS:
|
||||
if re.search(pattern, text):
|
||||
hits.append(f"{path.relative_to(ROOT)}: {label}")
|
||||
return hits
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import subprocess
|
||||
|
||||
db_check = subprocess.run(
|
||||
[sys.executable, str(ROOT / "scripts" / "check_database_imports.py")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if db_check.returncode != 0:
|
||||
print(db_check.stdout or db_check.stderr)
|
||||
return 1
|
||||
globs = [
|
||||
"Makefile",
|
||||
"Dockerfile",
|
||||
"docker-compose*.yml",
|
||||
".gitea/**/*.yaml",
|
||||
"CLAUDE.md",
|
||||
"README.md",
|
||||
"pyproject.toml",
|
||||
"devplacepy/**/*.py",
|
||||
"tests/**/*.py",
|
||||
"orchestrator.py",
|
||||
]
|
||||
errors = []
|
||||
for pattern in globs:
|
||||
for path in ROOT.glob(pattern):
|
||||
if path.is_file():
|
||||
errors.extend(scan_file(path))
|
||||
if PHASE in ("m3", "m6", "m7") and (ROOT / "devplacepy/main.py").exists():
|
||||
if PHASE == "m7":
|
||||
errors.append("devplacepy/main.py: file must be deleted at M7")
|
||||
elif PHASE == "m3":
|
||||
pass
|
||||
if errors:
|
||||
print("MONOLITH ARTIFACTS FOUND:")
|
||||
for line in sorted(set(errors)):
|
||||
print(f" {line}")
|
||||
return 1
|
||||
print("check_no_monolith: ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
60
scripts/migrate_db_imports.py
Normal file
60
scripts/migrate_db_imports.py
Normal file
@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SKIP_PREFIXES = (
|
||||
ROOT / "devplacepy" / "database",
|
||||
ROOT / "devplacepy_services" / "database",
|
||||
ROOT / "devplacepy" / "db_client.py",
|
||||
ROOT / "scripts",
|
||||
ROOT / "splitup.md",
|
||||
)
|
||||
|
||||
REPLACEMENTS = [
|
||||
(re.compile(r"\bfrom devplacepy\.database import\b"), "from devplacepy.db_client import"),
|
||||
(re.compile(r"\bfrom devplacepy\.database\.([a-z_]+) import\b"), r"from devplacepy.db_client import"),
|
||||
(re.compile(r"\bimport devplacepy\.database\b"), "import devplacepy.db_client"),
|
||||
]
|
||||
|
||||
|
||||
def should_skip(path: Path) -> bool:
|
||||
for prefix in SKIP_PREFIXES:
|
||||
if path == prefix or str(path).startswith(str(prefix) + "/"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def migrate_file(path: Path) -> bool:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
original = text
|
||||
for pattern, repl in REPLACEMENTS:
|
||||
text = pattern.sub(repl, text)
|
||||
if text != original:
|
||||
path.write_text(text, encoding="utf-8")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
changed = []
|
||||
for pattern in ("devplacepy/**/*.py", "tests/**/*.py", "devplacepy_services/**/*.py"):
|
||||
for path in ROOT.glob(pattern):
|
||||
if not path.is_file() or should_skip(path):
|
||||
continue
|
||||
if migrate_file(path):
|
||||
changed.append(path.relative_to(ROOT))
|
||||
if changed:
|
||||
print(f"migrated {len(changed)} files")
|
||||
for item in sorted(changed)[:40]:
|
||||
print(f" {item}")
|
||||
if len(changed) > 40:
|
||||
print(f" ... and {len(changed) - 40} more")
|
||||
else:
|
||||
print("no files changed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
92
scripts/refactor_to_microservices.py
Normal file
92
scripts/refactor_to_microservices.py
Normal file
@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import importlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SERVICES_DIR = ROOT / "devplacepy_services"
|
||||
|
||||
SERVICES = [
|
||||
"web",
|
||||
"database",
|
||||
"pubsub",
|
||||
"gateway",
|
||||
"jobs",
|
||||
"devii",
|
||||
"bot",
|
||||
"backup",
|
||||
"containers",
|
||||
"telegram",
|
||||
"email",
|
||||
"news",
|
||||
"gitea",
|
||||
"audit",
|
||||
"xmlrpc",
|
||||
]
|
||||
|
||||
|
||||
def list_services() -> int:
|
||||
for name in SERVICES:
|
||||
main_py = SERVICES_DIR / name / "main.py"
|
||||
status = "ok" if main_py.exists() else "missing"
|
||||
print(f"{name:12} {status}")
|
||||
return 0
|
||||
|
||||
|
||||
def verify_stubs() -> int:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
errors: list[str] = []
|
||||
for name in SERVICES:
|
||||
main_py = SERVICES_DIR / name / "main.py"
|
||||
if not main_py.exists():
|
||||
errors.append(f"{name}: missing main.py")
|
||||
continue
|
||||
module_name = f"devplacepy_services.{name}.main"
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except Exception as exc:
|
||||
errors.append(f"{name}: import failed ({exc})")
|
||||
continue
|
||||
app = getattr(module, "app", None)
|
||||
if app is None:
|
||||
errors.append(f"{name}: no app export")
|
||||
continue
|
||||
try:
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
client = TestClient(app)
|
||||
resp = client.get("/health")
|
||||
if resp.status_code != 200:
|
||||
errors.append(f"{name}: /health returned {resp.status_code}")
|
||||
continue
|
||||
data = resp.json()
|
||||
for key in ("service", "status", "uptime_s", "version", "deps", "stats"):
|
||||
if key not in data:
|
||||
errors.append(f"{name}: health missing key {key}")
|
||||
except Exception as exc:
|
||||
errors.append(f"{name}: health check failed ({exc})")
|
||||
if errors:
|
||||
print("verify: failed")
|
||||
for err in errors:
|
||||
print(f" {err}")
|
||||
return 1
|
||||
print("verify: ok")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(prog="refactor_to_microservices.py")
|
||||
parser.add_argument("--list", action="store_true")
|
||||
parser.add_argument("--verify", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.list:
|
||||
return list_services()
|
||||
if args.verify:
|
||||
return verify_stubs()
|
||||
parser.print_help()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Reference in New Issue
Block a user