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