Initialisiere Explor.Code.Law mit RII-Pipeline und Strato-Deploy.
Some checks failed
Deploy explore / deploy (push) Failing after 18s
Some checks failed
Deploy explore / deploy (push) Failing after 18s
Entscheidungen als Markdown, Normen-Index und schlankes Git-Sync nach /opt/explore (Gitea Actions, ohne Docker). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
59
.gitea/workflows/deploy.yml
Normal file
59
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
# Sync Explor.Code.Law nach /opt/explore
|
||||
# Gitea Secret: SSH_PRIVATE_KEY
|
||||
#
|
||||
# Schlankes Daten-Deploy: nur Git-Checkout, kein Docker.
|
||||
# Kein git clean / kein reset --hard — ge_xml/ und .env bleiben.
|
||||
|
||||
name: Deploy explore
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: explore-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: python:3.13-bookworm
|
||||
steps:
|
||||
- name: Deploy to Server
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: 82.165.92.92
|
||||
username: root
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
command_timeout: 15m
|
||||
script: |
|
||||
set -euo pipefail
|
||||
echo "Starting explore deployment..."
|
||||
mkdir -p /opt/explore
|
||||
cd /opt/explore
|
||||
|
||||
if [ ! -d .git ]; then
|
||||
git init
|
||||
git remote add origin https://git.coded.law/michaelkole/explore.git || true
|
||||
fi
|
||||
git remote set-url origin https://git.coded.law/michaelkole/explore.git
|
||||
git fetch origin main
|
||||
# Tracked Dateien aktualisieren. Kein reset --hard, kein git clean:
|
||||
# ge_xml/ und .env bleiben unangetastet.
|
||||
git checkout origin/main -- .
|
||||
|
||||
mkdir -p ge_md data ge_xml kommentar
|
||||
chmod 600 .env 2>/dev/null || true
|
||||
if [ ! -f .env ] && [ -f env.example ]; then
|
||||
cp env.example .env
|
||||
chmod 600 .env
|
||||
fi
|
||||
grep -q "^LAWGIT_ROOT=" .env 2>/dev/null || echo "LAWGIT_ROOT=/opt/lawgit" >> .env
|
||||
grep -q "^DOMAIN=" .env 2>/dev/null || echo "DOMAIN=explore.coded.law" >> .env
|
||||
|
||||
echo "ge_md files: $(find ge_md -name '*.md' 2>/dev/null | wc -l)"
|
||||
echo "index: $([ -f data/normen_index.json ] && wc -c < data/normen_index.json || echo missing) bytes"
|
||||
echo "ge_xml files (local disk, not from git): $(find ge_xml -type f 2>/dev/null | wc -l)"
|
||||
echo "Deployment complete!"
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Roh-XML/ZIP von Rechtsprechung im Internet
|
||||
ge_xml/
|
||||
*.zip
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.venv/
|
||||
venv/
|
||||
.env
|
||||
.Python
|
||||
|
||||
# OS / IDE
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Test / Cache
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
*.log
|
||||
126
INFRASTRUCTURE.md
Normal file
126
INFRASTRUCTURE.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Infrastruktur — Explor.Code.Law
|
||||
|
||||
Gerichtsentscheidungen und Normen-Index auf dem Strato-VServer, analog zu Lawgit.
|
||||
Aktuell **Daten + Pipeline** (kein UI). Domain und Port sind für später reserviert.
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
Lokal / CI
|
||||
→ Gitea git.coded.law/michaelkole/explore
|
||||
→ Checkout /opt/explore
|
||||
├── ge_md/ # Markdown-Volltexte (im Git)
|
||||
├── data/ # normen_index.json (im Git)
|
||||
├── ge_xml/ # Roh-ZIP/XML (NICHT im Git, nur auf Disk)
|
||||
├── extract_rii.py …
|
||||
└── LAWGIT_ROOT → /opt/lawgit
|
||||
```
|
||||
|
||||
Gesetze bleiben in Lawgit (`/opt/lawgit/laws_md/`). Explore kopiert sie nicht.
|
||||
|
||||
## Pfade
|
||||
|
||||
| Was | Pfad |
|
||||
|-----|------|
|
||||
| Checkout | `/opt/explore/` |
|
||||
| Entscheidungen (Markdown) | `/opt/explore/ge_md/` |
|
||||
| Normen-Index | `/opt/explore/data/normen_index.json` |
|
||||
| Rohdaten (gitignoriert) | `/opt/explore/ge_xml/` |
|
||||
| Lawgit (Read-only Bezug) | `/opt/lawgit/` |
|
||||
| Optionale Env | `/opt/explore/.env` (Mode 600) |
|
||||
|
||||
**Wichtig:** Deploy darf `ge_xml/` und `.env` nie löschen (`git clean` / `reset --hard` vermeiden).
|
||||
|
||||
## Domain / Port (vorbereitet)
|
||||
|
||||
| Variable | Wert | Status |
|
||||
|----------|------|--------|
|
||||
| Domain | `explore.coded.law` | DNS + Certbot noch offen |
|
||||
| Frontend-Port (später) | `127.0.0.1:3097` | frei (Stand Server-Check) |
|
||||
| UI / Apache / Docker | — | bewusst noch nicht eingerichtet |
|
||||
|
||||
## Gitea
|
||||
|
||||
- Host: `https://git.coded.law`
|
||||
- Repo (Ziel): `michaelkole/explore`
|
||||
- Deploy-Workflow: `.gitea/workflows/deploy.yml`
|
||||
- Secret: `SSH_PRIVATE_KEY` (siehe `setup-ssh-deploy.sh`)
|
||||
|
||||
SSH zum Server: `ssh -i ~/.ssh/gitea_deploy root@82.165.92.92`
|
||||
|
||||
## Sync & Deploy
|
||||
|
||||
### Erstinstallation (einmalig)
|
||||
|
||||
1. Gitea-Repo `michaelkole/explore` anlegen und lokalen Stand pushen.
|
||||
2. Auf dem Server:
|
||||
|
||||
```bash
|
||||
git clone https://git.coded.law/michaelkole/explore.git /opt/explore
|
||||
cd /opt/explore
|
||||
cp env.example .env && chmod 600 .env # optional
|
||||
# optional: Roh-XML von lokal nachziehen
|
||||
# ./sync_to_server.sh --xml-only
|
||||
```
|
||||
|
||||
Oder lokal: `./deploy.sh` (legt `/opt/explore` an und pullt).
|
||||
|
||||
3. CI: `./setup-ssh-deploy.sh` (Gitea-API-Token mit repo-Rechten).
|
||||
|
||||
### Laufendes Update (Git)
|
||||
|
||||
Push auf `main` → Workflow aktualisiert tracked Dateien unter `/opt/explore`
|
||||
(`ge_md/`, `data/`, Skripte). `ge_xml/` bleibt unberührt.
|
||||
|
||||
Manuell:
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/gitea_deploy root@82.165.92.92
|
||||
cd /opt/explore
|
||||
git fetch origin main
|
||||
git checkout origin/main -- .
|
||||
```
|
||||
|
||||
### Roh-XML (`ge_xml/`)
|
||||
|
||||
Liegt nicht in Git. Nach lokaler Extraktion auf den Server spiegeln:
|
||||
|
||||
```bash
|
||||
./sync_to_server.sh --xml-only
|
||||
# oder voller Bootstrap (ohne .venv):
|
||||
./sync_to_server.sh
|
||||
```
|
||||
|
||||
### Index auf dem Server neu bauen
|
||||
|
||||
```bash
|
||||
cd /opt/explore
|
||||
python3 -m pip install -r requirements.txt --break-system-packages # oder venv
|
||||
python3 index_normen.py --lawgit /opt/lawgit
|
||||
```
|
||||
|
||||
## Env (`env.example` → `.env`)
|
||||
|
||||
| Variable | Default | Zweck |
|
||||
|----------|---------|--------|
|
||||
| `LAWGIT_ROOT` | `/opt/lawgit` | Pfad zu `laws_md/` für `index_normen.py` |
|
||||
| `DOMAIN` | `explore.coded.law` | reserviert für späteres UI |
|
||||
|
||||
## Nützliche Befehle
|
||||
|
||||
```bash
|
||||
# Bestand prüfen
|
||||
ssh -i ~/.ssh/gitea_deploy root@82.165.92.92 'du -sh /opt/explore/ge_md /opt/explore/ge_xml /opt/explore/data; find /opt/explore/ge_md -name "*.md" | wc -l'
|
||||
|
||||
# Extraktion auf dem Server (braucht Netz + Platz)
|
||||
cd /opt/explore && python3 extract_rii.py --limit 5 --court bgh,bverfg
|
||||
```
|
||||
|
||||
## Abgrenzung zu Lawgit
|
||||
|
||||
| | lawgit | explore |
|
||||
|--|--------|---------|
|
||||
| Checkout | `/opt/lawgit` | `/opt/explore` |
|
||||
| Inhalt | `laws_md/`, UI, Postgres | `ge_md/`, Index, später Kommentar |
|
||||
| Deploy | Docker Compose + Apache | Git-Sync (schlank) |
|
||||
| Domain | lawgit.coded.law | explore.coded.law (geplant) |
|
||||
69
README.md
Normal file
69
README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Explor.Code.Law
|
||||
|
||||
Kommentar- und Rechtsprechungsschicht auf Basis von [lawgit](../lawgit).
|
||||
|
||||
Gesetze kommen aus Lawgit (`laws_md/`). Hier landen Gerichtsentscheidungen
|
||||
als Markdown-Volltext und später Kommentare (Beck/Grüneberg-Stil), die sich
|
||||
mit neuer Rechtsprechung fort schreiben.
|
||||
|
||||
## Verhältnis zu Lawgit
|
||||
|
||||
| Repo | Inhalt |
|
||||
|------|--------|
|
||||
| **lawgit** | geltender Wortlaut (`laws_md/`), Fassungsgeschichte, Normzeitachse |
|
||||
| **explore** | Gerichtsentscheidungen (`ge_md/`), Normen-Index, Kommentare |
|
||||
|
||||
Gesetze werden **nicht** kopiert. Explore liest sie über `LAWGIT_ROOT`
|
||||
(Default: `../lawgit`).
|
||||
|
||||
## Schnellstart
|
||||
|
||||
```bash
|
||||
cd /Users/michaelhermann/Source/law/coded/explore
|
||||
python3 -m pip install -r requirements.txt
|
||||
|
||||
# Rauchtest: fünf BGH- und BVerfG-Entscheidungen
|
||||
python3 extract_rii.py --limit 5 --court bgh,bverfg
|
||||
|
||||
# Normen an Lawgit-Paragraphen hängen
|
||||
python3 index_normen.py --lawgit ../lawgit
|
||||
|
||||
# Offline-Tests
|
||||
python3 -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
## Struktur
|
||||
|
||||
```
|
||||
explore/
|
||||
├── ge_md/ # Entscheidungen als Markdown (Volltext)
|
||||
├── ge_xml/ # Roh-ZIP/XML (gitignoriert)
|
||||
├── kommentar/ # später: Kommentartexte pro Paragraph
|
||||
├── data/ # normen_index.json
|
||||
├── extract_rii.py # Download + Konvertierung
|
||||
├── rii_to_markdown.py
|
||||
├── index_normen.py
|
||||
└── docs/
|
||||
├── QUELLEN.md
|
||||
└── EINARBEITUNG.md
|
||||
```
|
||||
|
||||
## Server (Strato)
|
||||
|
||||
Persistenz unter `/opt/explore` auf `82.165.92.92` (Domain geplant:
|
||||
`explore.coded.law`). Details: [INFRASTRUCTURE.md](INFRASTRUCTURE.md).
|
||||
|
||||
```bash
|
||||
# Bootstrap / ge_xml spiegeln (SSH-Key gitea_deploy)
|
||||
./sync_to_server.sh
|
||||
./sync_to_server.sh --xml-only
|
||||
```
|
||||
|
||||
## Quellen
|
||||
|
||||
Primär: [rechtsprechung-im-internet.de](https://www.rechtsprechung-im-internet.de/)
|
||||
(BVerfG und oberste Bundesgerichte ab 2010). Details und Lücken:
|
||||
[docs/QUELLEN.md](docs/QUELLEN.md).
|
||||
|
||||
Wie neue Entscheidungen den Kommentar treffen:
|
||||
[docs/EINARBEITUNG.md](docs/EINARBEITUNG.md).
|
||||
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
420
data/normen_index.json
Normal file
420
data/normen_index.json
Normal file
@@ -0,0 +1,420 @@
|
||||
{
|
||||
"lawgit_root": "/Users/michaelhermann/Source/law/coded/lawgit",
|
||||
"ge_md": "/Users/michaelhermann/Source/law/coded/explore/ge_md",
|
||||
"entscheidungen": 9,
|
||||
"normen": {
|
||||
"bverfgg/§23": [
|
||||
{
|
||||
"doknr": "KVRE000022442",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "1 BvR 2231/23",
|
||||
"datum": "2024-07-02",
|
||||
"path": "bverfg/2024/1-bvr-2231-23.md",
|
||||
"doktyp": "Nichtannahmebeschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 23 Abs 1 S 2 BVerfGG",
|
||||
"gesetz": "bverfgg",
|
||||
"paragraph": "23",
|
||||
"lawgit": "laws_md/bverfgg/§23.md",
|
||||
"mapped": true
|
||||
},
|
||||
{
|
||||
"doknr": "KVRE000012442",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "2 BvR 923/24",
|
||||
"datum": "2024-07-12",
|
||||
"path": "bverfg/2024/2-bvr-923-24.md",
|
||||
"doktyp": "Ablehnung einstweilige Anordnung",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 23 Abs 1 S 2 BVerfGG",
|
||||
"gesetz": "bverfgg",
|
||||
"paragraph": "23",
|
||||
"lawgit": "laws_md/bverfgg/§23.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"bverfgg/§32": [
|
||||
{
|
||||
"doknr": "KVRE000012442",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "2 BvR 923/24",
|
||||
"datum": "2024-07-12",
|
||||
"path": "bverfg/2024/2-bvr-923-24.md",
|
||||
"doktyp": "Ablehnung einstweilige Anordnung",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 32 Abs 1 BVerfGG",
|
||||
"gesetz": "bverfgg",
|
||||
"paragraph": "32",
|
||||
"lawgit": "laws_md/bverfgg/§32.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"bverfgg/§90": [
|
||||
{
|
||||
"doknr": "KVRE000012542",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "1 BvR 2268/23",
|
||||
"datum": "2024-11-20",
|
||||
"path": "bverfg/2024/1-bvr-2268-23.md",
|
||||
"doktyp": "Kammerbeschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 90 BVerfGG",
|
||||
"gesetz": "bverfgg",
|
||||
"paragraph": "90",
|
||||
"lawgit": "laws_md/bverfgg/§90.md",
|
||||
"mapped": true
|
||||
},
|
||||
{
|
||||
"doknr": "KVRE000032442",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "1 BvR 875/23",
|
||||
"datum": "2024-06-20",
|
||||
"path": "bverfg/2024/1-bvr-875-23.md",
|
||||
"doktyp": "Prozesskostenhilfebeschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 90 BVerfGG",
|
||||
"gesetz": "bverfgg",
|
||||
"paragraph": "90",
|
||||
"lawgit": "laws_md/bverfgg/§90.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"bverfgg/§92": [
|
||||
{
|
||||
"doknr": "KVRE000022442",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "1 BvR 2231/23",
|
||||
"datum": "2024-07-02",
|
||||
"path": "bverfg/2024/1-bvr-2231-23.md",
|
||||
"doktyp": "Nichtannahmebeschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 92 BVerfGG",
|
||||
"gesetz": "bverfgg",
|
||||
"paragraph": "92",
|
||||
"lawgit": "laws_md/bverfgg/§92.md",
|
||||
"mapped": true
|
||||
},
|
||||
{
|
||||
"doknr": "KVRE000012442",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "2 BvR 923/24",
|
||||
"datum": "2024-07-12",
|
||||
"path": "bverfg/2024/2-bvr-923-24.md",
|
||||
"doktyp": "Ablehnung einstweilige Anordnung",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 92 BVerfGG",
|
||||
"gesetz": "bverfgg",
|
||||
"paragraph": "92",
|
||||
"lawgit": "laws_md/bverfgg/§92.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"inso/§315": [
|
||||
{
|
||||
"doknr": "JURE100055035",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "IX ZB 76/09",
|
||||
"datum": "2010-01-14",
|
||||
"path": "bgh/2010/ix-zb-76-09.md",
|
||||
"doktyp": "Beschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 315 InsO",
|
||||
"gesetz": "inso",
|
||||
"paragraph": "315",
|
||||
"lawgit": "laws_md/inso/§315.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"inso/§4": [
|
||||
{
|
||||
"doknr": "JURE100055033",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "IX ZB 72/08",
|
||||
"datum": "2010-01-14",
|
||||
"path": "bgh/2010/ix-zb-72-08.md",
|
||||
"doktyp": "Beschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 4 InsO",
|
||||
"gesetz": "inso",
|
||||
"paragraph": "4",
|
||||
"lawgit": "laws_md/inso/§4.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"jgg/§42": [
|
||||
{
|
||||
"doknr": "JURE100055051",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "2 ARs 569/09",
|
||||
"datum": "2010-01-13",
|
||||
"path": "bgh/2010/2-ars-569-09.md",
|
||||
"doktyp": "Beschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 42 Abs 3 JGG",
|
||||
"gesetz": "jgg",
|
||||
"paragraph": "42",
|
||||
"lawgit": "laws_md/jgg/§42.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"stgb/§20": [
|
||||
{
|
||||
"doknr": "JURE100055074",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "2 StR 447/09",
|
||||
"datum": "2010-01-13",
|
||||
"path": "bgh/2010/2-str-447-09.md",
|
||||
"doktyp": "Beschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 20 StGB",
|
||||
"gesetz": "stgb",
|
||||
"paragraph": "20",
|
||||
"lawgit": "laws_md/stgb/§20.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"stgb/§212": [
|
||||
{
|
||||
"doknr": "JURE100055072",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "2 StR 428/09",
|
||||
"datum": "2010-01-13",
|
||||
"path": "bgh/2010/2-str-428-09.md",
|
||||
"doktyp": "Urteil",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 212 StGB",
|
||||
"gesetz": "stgb",
|
||||
"paragraph": "212",
|
||||
"lawgit": "laws_md/stgb/§212.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"stgb/§224": [
|
||||
{
|
||||
"doknr": "JURE100055074",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "2 StR 447/09",
|
||||
"datum": "2010-01-13",
|
||||
"path": "bgh/2010/2-str-447-09.md",
|
||||
"doktyp": "Beschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 224 StGB",
|
||||
"gesetz": "stgb",
|
||||
"paragraph": "224",
|
||||
"lawgit": "laws_md/stgb/§224.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"stgb/§227": [
|
||||
{
|
||||
"doknr": "JURE100055072",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "2 StR 428/09",
|
||||
"datum": "2010-01-13",
|
||||
"path": "bgh/2010/2-str-428-09.md",
|
||||
"doktyp": "Urteil",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 227 StGB",
|
||||
"gesetz": "stgb",
|
||||
"paragraph": "227",
|
||||
"lawgit": "laws_md/stgb/§227.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"stgb/§241": [
|
||||
{
|
||||
"doknr": "JURE100055074",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "2 StR 447/09",
|
||||
"datum": "2010-01-13",
|
||||
"path": "bgh/2010/2-str-447-09.md",
|
||||
"doktyp": "Beschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 241 StGB",
|
||||
"gesetz": "stgb",
|
||||
"paragraph": "241",
|
||||
"lawgit": "laws_md/stgb/§241.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"zpo/§114": [
|
||||
{
|
||||
"doknr": "KVRE000032442",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "1 BvR 875/23",
|
||||
"datum": "2024-06-20",
|
||||
"path": "bverfg/2024/1-bvr-875-23.md",
|
||||
"doktyp": "Prozesskostenhilfebeschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 114 Abs 1 S 1 ZPO",
|
||||
"gesetz": "zpo",
|
||||
"paragraph": "114",
|
||||
"lawgit": "laws_md/zpo/§114.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"zpo/§13": [
|
||||
{
|
||||
"doknr": "JURE100055033",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "IX ZB 72/08",
|
||||
"datum": "2010-01-14",
|
||||
"path": "bgh/2010/ix-zb-72-08.md",
|
||||
"doktyp": "Beschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 13 ZPO",
|
||||
"gesetz": "zpo",
|
||||
"paragraph": "13",
|
||||
"lawgit": "laws_md/zpo/§13.md",
|
||||
"mapped": true
|
||||
},
|
||||
{
|
||||
"doknr": "JURE100055035",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "IX ZB 76/09",
|
||||
"datum": "2010-01-14",
|
||||
"path": "bgh/2010/ix-zb-76-09.md",
|
||||
"doktyp": "Beschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 13 ZPO",
|
||||
"gesetz": "zpo",
|
||||
"paragraph": "13",
|
||||
"lawgit": "laws_md/zpo/§13.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"zpo/§16": [
|
||||
{
|
||||
"doknr": "JURE100055035",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "IX ZB 76/09",
|
||||
"datum": "2010-01-14",
|
||||
"path": "bgh/2010/ix-zb-76-09.md",
|
||||
"doktyp": "Beschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 16 ZPO",
|
||||
"gesetz": "zpo",
|
||||
"paragraph": "16",
|
||||
"lawgit": "laws_md/zpo/§16.md",
|
||||
"mapped": true
|
||||
}
|
||||
],
|
||||
"zpo/§251": [
|
||||
{
|
||||
"doknr": "JURE100055033",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "IX ZB 72/08",
|
||||
"datum": "2010-01-14",
|
||||
"path": "bgh/2010/ix-zb-72-08.md",
|
||||
"doktyp": "Beschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 251 ZPO",
|
||||
"gesetz": "zpo",
|
||||
"paragraph": "251",
|
||||
"lawgit": "laws_md/zpo/§251.md",
|
||||
"mapped": true
|
||||
},
|
||||
{
|
||||
"doknr": "KVRE000012542",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "1 BvR 2268/23",
|
||||
"datum": "2024-11-20",
|
||||
"path": "bverfg/2024/1-bvr-2268-23.md",
|
||||
"doktyp": "Kammerbeschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 251 ZPO",
|
||||
"gesetz": "zpo",
|
||||
"paragraph": "251",
|
||||
"lawgit": "laws_md/zpo/§251.md",
|
||||
"mapped": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"unmapped": [
|
||||
{
|
||||
"doknr": "JURE100055035",
|
||||
"gericht": "BGH",
|
||||
"ebene": "bund",
|
||||
"aktenzeichen": "IX ZB 76/09",
|
||||
"datum": "2010-01-14",
|
||||
"path": "bgh/2010/ix-zb-76-09.md",
|
||||
"doktyp": "Beschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 3 EGV 1346/2000",
|
||||
"gesetz": "",
|
||||
"paragraph": "",
|
||||
"lawgit": null,
|
||||
"mapped": false,
|
||||
"grund": "kein_lawgit_pfad"
|
||||
},
|
||||
{
|
||||
"doknr": "KVRE000022442",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "1 BvR 2231/23",
|
||||
"datum": "2024-07-02",
|
||||
"path": "bverfg/2024/1-bvr-2231-23.md",
|
||||
"doktyp": "Nichtannahmebeschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "Art 4 Abs 1 GG",
|
||||
"gesetz": "gg",
|
||||
"paragraph": "4",
|
||||
"lawgit": null,
|
||||
"mapped": false,
|
||||
"grund": "kein_lawgit_pfad"
|
||||
},
|
||||
{
|
||||
"doknr": "KVRE000022442",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "1 BvR 2231/23",
|
||||
"datum": "2024-07-02",
|
||||
"path": "bverfg/2024/1-bvr-2231-23.md",
|
||||
"doktyp": "Nichtannahmebeschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "Art 4 Abs 2 GG",
|
||||
"gesetz": "gg",
|
||||
"paragraph": "4",
|
||||
"lawgit": null,
|
||||
"mapped": false,
|
||||
"grund": "kein_lawgit_pfad"
|
||||
},
|
||||
{
|
||||
"doknr": "KVRE000012542",
|
||||
"gericht": "BVerfG",
|
||||
"ebene": "verfg",
|
||||
"aktenzeichen": "1 BvR 2268/23",
|
||||
"datum": "2024-11-20",
|
||||
"path": "bverfg/2024/1-bvr-2268-23.md",
|
||||
"doktyp": "Kammerbeschluss",
|
||||
"hat_leitsatz": false,
|
||||
"roh": "§ 17 EnSiG",
|
||||
"gesetz": "ensig",
|
||||
"paragraph": "17",
|
||||
"lawgit": null,
|
||||
"mapped": false,
|
||||
"grund": "kein_lawgit_pfad"
|
||||
}
|
||||
]
|
||||
}
|
||||
60
deploy.sh
Executable file
60
deploy.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
# Erstinstallation / Sync von Explor.Code.Law auf dem Strato-VServer.
|
||||
# Kein Docker, kein Apache — nur Git-Checkout unter /opt/explore.
|
||||
# ge_xml/ und .env werden nicht gelöscht.
|
||||
set -euo pipefail
|
||||
|
||||
echo "explore – Server-Sync"
|
||||
echo "====================="
|
||||
|
||||
PROJECT_DIR="/opt/explore"
|
||||
GITEA_URL="https://git.coded.law/michaelkole/explore.git"
|
||||
REMOTE_BRANCH="main"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ] && [ ! -w "$(dirname "$PROJECT_DIR")" ]; then
|
||||
echo "Hinweis: Skript typischerweise als root auf dem Server ausführen,"
|
||||
echo " oder lokal sync_to_server.sh für rsync nutzen."
|
||||
fi
|
||||
|
||||
mkdir -p "$PROJECT_DIR"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
if [ ! -d .git ]; then
|
||||
if git ls-remote "$GITEA_URL" HEAD &>/dev/null; then
|
||||
echo "Klone $GITEA_URL → $PROJECT_DIR"
|
||||
# Verzeichnis kann schon Dateien haben (Bootstrap-rsync) → init + fetch
|
||||
git init
|
||||
git remote add origin "$GITEA_URL" 2>/dev/null || git remote set-url origin "$GITEA_URL"
|
||||
git fetch origin "$REMOTE_BRANCH"
|
||||
git checkout -B "$REMOTE_BRANCH" "origin/$REMOTE_BRANCH"
|
||||
else
|
||||
echo "Gitea-Repo noch nicht erreichbar: $GITEA_URL"
|
||||
echo "Repo anlegen, pushen, dann dieses Skript erneut ausführen."
|
||||
echo "Bis dahin: lokal ./sync_to_server.sh nutzen."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Aktualisiere tracked Dateien (ohne git clean)…"
|
||||
git remote set-url origin "$GITEA_URL" 2>/dev/null || true
|
||||
git fetch origin "$REMOTE_BRANCH"
|
||||
# Tracked Dateien aktualisieren. Kein reset --hard, kein git clean:
|
||||
# ge_xml/ und .env bleiben unangetastet.
|
||||
git checkout "origin/$REMOTE_BRANCH" -- .
|
||||
fi
|
||||
|
||||
mkdir -p ge_md data ge_xml kommentar
|
||||
|
||||
if [ ! -f .env ] && [ -f env.example ]; then
|
||||
cp env.example .env
|
||||
chmod 600 .env
|
||||
echo ".env aus env.example angelegt"
|
||||
fi
|
||||
chmod 600 .env 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "Fertig: $PROJECT_DIR"
|
||||
echo " ge_md: $(find ge_md -name '*.md' 2>/dev/null | wc -l | tr -d ' ') Dateien"
|
||||
echo " ge_xml: $(find ge_xml -type f 2>/dev/null | wc -l | tr -d ' ') Dateien"
|
||||
echo " Index: $([ -f data/normen_index.json ] && echo ja || echo fehlt)"
|
||||
echo ""
|
||||
echo "Optional Index: python3 index_normen.py --lawgit /opt/lawgit"
|
||||
75
docs/EINARBEITUNG.md
Normal file
75
docs/EINARBEITUNG.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Einarbeitungsmodus
|
||||
|
||||
Neue Informationen **ändern nicht das Gesetz** (das bleibt in Lawgit),
|
||||
sondern den **Kommentar** zum Paragraphen. Gerichtsentscheidungen aus
|
||||
`ge_md/` werden in drei Stufen verarbeitet.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
GE[Neue Entscheidung ge_md]
|
||||
Fund[1 Fundstelle Index]
|
||||
Gewicht[2 Gewicht nach Ebene]
|
||||
Edit[3 Kommentar-Edit]
|
||||
GE --> Fund
|
||||
Fund --> Gewicht
|
||||
Gewicht -->|Revisionskandidat| Edit
|
||||
Gewicht -->|nur Fundstelle| Stop[Index fertig]
|
||||
```
|
||||
|
||||
## Stufe 1 — Fundstelle (immer)
|
||||
|
||||
Aus dem Feld `norm` der RII-XML (im YAML-Kopf als `normen`) werden
|
||||
zitierte Stellen gelesen und gegen `LAWGIT_ROOT/laws_md/<slug>/§N.md`
|
||||
gematcht.
|
||||
|
||||
Werkzeug: `python3 index_normen.py --lawgit ../lawgit`
|
||||
|
||||
Ergebnis: `data/normen_index.json`
|
||||
|
||||
- Treffer → Eintrag unter Schlüssel `bgb/§242` usw.
|
||||
- Kein Treffer → Liste `unmapped` (manuelle Aliase analog Lawgit
|
||||
`norm_registry.py`)
|
||||
|
||||
Jeder Index-Eintrag trägt `ebene`, `gericht`, `doknr`, `path`, `doktyp`
|
||||
und ob ein Leitsatz im Markdown steht.
|
||||
|
||||
## Stufe 2 — Gewicht nach Ebene
|
||||
|
||||
Steuert, ob der Kommentar *bearbeitet* wird oder die Entscheidung nur
|
||||
als Fundstelle hängt.
|
||||
|
||||
| Ebene | Wert in YAML | Regel |
|
||||
|-------|--------------|--------|
|
||||
| **VerfG** | `verfg` | Immer Revisionskandidat. Bindungswirkung nach § 31 BVerfGG; kann die Lesart einer Norm kippen. |
|
||||
| **Bund** | `bund` | Revisionskandidat, wenn Leitsatz vorhanden oder `doktyp` auf Leitentscheidung / Großen Senat hindeutet; sonst nur Fundstelle. |
|
||||
| **OLG** | `olg` | Nur Fundstelle, außer **Divergenz** (abweichende OLG-Linien ohne BGH) oder Lücke auf Bundesebene. (Extraktion noch Stub.) |
|
||||
|
||||
Ableitung der Ebene aus `gertyp`:
|
||||
|
||||
- `BVerfG` → `verfg`
|
||||
- `BGH`, `BVerwG`, `BFH`, `BAG`, `BSG`, `BPatG` → `bund`
|
||||
- später `OLG …` → `olg`
|
||||
|
||||
## Stufe 3 — Kommentar-Edit (noch nicht implementiert)
|
||||
|
||||
Pro Paragraph eine Datei `kommentar/<gesetz>/§N.md` mit Randnummern.
|
||||
|
||||
Jede Einarbeitung soll ein Git-Commit mit Trailer werden:
|
||||
|
||||
```
|
||||
RII-Doknr: KORE…
|
||||
```
|
||||
|
||||
analog zu `BGBl-Id:` in Lawgit — damit nachvollziehbar bleibt, welche
|
||||
Entscheidung den Kommentartext bewegt hat.
|
||||
|
||||
Der eigentliche Text (LLM oder Hand) ist der nächste Projektschritt.
|
||||
Dieser Modus dokumentiert nur die Regeln, damit der Lauf nicht neu
|
||||
erfunden wird.
|
||||
|
||||
## Praxis-Reihenfolge
|
||||
|
||||
1. `extract_rii.py` — Entscheidungen holen
|
||||
2. `index_normen.py` — Fundstellen bauen
|
||||
3. Später: Revisionskandidaten aus dem Index filtern (`ebene=verfg` oder
|
||||
Bund mit Leitsatz) und Kommentardateien fortschreiben
|
||||
58
docs/QUELLEN.md
Normal file
58
docs/QUELLEN.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Quellen
|
||||
|
||||
## Primär: Rechtsprechung im Internet (RII)
|
||||
|
||||
Portal: [https://www.rechtsprechung-im-internet.de/](https://www.rechtsprechung-im-internet.de/)
|
||||
|
||||
| Ressource | URL |
|
||||
|-----------|-----|
|
||||
| Inhaltsverzeichnis | `https://www.rechtsprechung-im-internet.de/rii-toc.xml` |
|
||||
| DTD | `https://www.rechtsprechung-im-internet.de/dtd/v1/rii-dok.dtd` |
|
||||
| Dokument | ZIP mit einer XML-Datei pro Entscheidung |
|
||||
|
||||
**Gerichte (amtlich, ausgewählt, anonymisiert, ab 2010):**
|
||||
|
||||
- Bundesverfassungsgericht (BVerfG)
|
||||
- Bundesgerichtshof (BGH)
|
||||
- Bundesverwaltungsgericht (BVerwG)
|
||||
- Bundesfinanzhof (BFH)
|
||||
- Bundesarbeitsgericht (BAG)
|
||||
- Bundessozialgericht (BSG)
|
||||
- Bundespatentgericht (BPatG)
|
||||
|
||||
Nutzung: amtliche Werke, gemeinfrei nach § 5 UrhG. Weiterverwendung der
|
||||
bereitgestellten Formate ist ausdrücklich vorgesehen.
|
||||
|
||||
Wichtige XML-Felder: `doknr`, `ecli`, `gertyp`, `spruchkoerper`,
|
||||
`entsch-datum`, `aktenzeichen`, `doktyp`, `norm`, `leitsatz`, `tenor`,
|
||||
`tatbestand`, `entscheidungsgruende` / `gruende`, `abwmeinung`, `vorinstanz`.
|
||||
|
||||
## Gesetzesbasis: Lawgit
|
||||
|
||||
Geltender Wortlaut und Paragraphenpfade: `LAWGIT_ROOT/laws_md/`
|
||||
(Default `../lawgit`). Explore kopiert Gesetze nicht.
|
||||
|
||||
## Bewusste Lücken
|
||||
|
||||
### OLG / LG
|
||||
|
||||
RII enthält **keine** systematische OLG-/LG-Sammlung. Das Datenmodell kennt
|
||||
trotzdem `ebene: olg`. In Bundesentscheidungen speichern wir `vorinstanz`
|
||||
(oft OLG-Aktenzeichen) schon mit. Ein eigener OLG-Extraktor folgt erst, wenn
|
||||
eine konkrete Landesquelle feststeht.
|
||||
|
||||
### BVerfG vor 2010
|
||||
|
||||
RII startet 2010. Ältere BVerfGE / Gerichtssite sind Phase 2.
|
||||
|
||||
### NeuRIS
|
||||
|
||||
Die Testphase unter `testphase.rechtsinformationen.bund.de` bietet eine
|
||||
JSON-API, der Datensatz ist noch unvollständig. Nicht als Primärquelle;
|
||||
höchstens später Abgleich zu RII.
|
||||
|
||||
## Volumen und Höflichkeit
|
||||
|
||||
Rund 70–80 000 Entscheidungen. `extract_rii.py` ist wiederaufsetzbar
|
||||
(überspringt bekannte `doknr`), mit `--court`, `--since`, `--limit` und
|
||||
Delay (Default 0,4 s).
|
||||
5
env.example
Normal file
5
env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# Server-Env für /opt/explore/.env (nicht committen)
|
||||
# cp env.example .env && chmod 600 .env
|
||||
|
||||
LAWGIT_ROOT=/opt/lawgit
|
||||
DOMAIN=explore.coded.law
|
||||
308
extract_rii.py
Normal file
308
extract_rii.py
Normal file
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Gerichtsentscheidungen von rechtsprechung-im-internet.de holen.
|
||||
|
||||
python3 extract_rii.py --limit 5 --court bgh,bverfg
|
||||
python3 extract_rii.py --court bgh --since 2020
|
||||
python3 extract_rii.py # voller Bestand (lang)
|
||||
|
||||
Wiederaufsetzbar: vorhandene doknr in ge_md/ werden übersprungen.
|
||||
Roh-ZIP/XML landen in ge_xml/ (gitignoriert).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from rii_to_markdown import (
|
||||
COURT_MAP,
|
||||
convert_xml_to_markdown_file,
|
||||
iso_date,
|
||||
parse_rii_xml,
|
||||
relative_path,
|
||||
)
|
||||
|
||||
TOC_URL = "https://www.rechtsprechung-im-internet.de/rii-toc.xml"
|
||||
UA = "explor-code-law/0.1 (https://git.coded.law; RII Open Data)"
|
||||
SESSION = requests.Session()
|
||||
SESSION.headers.update({"User-Agent": UA})
|
||||
|
||||
# Kurzname → Anzeigewert in TOC <gericht>
|
||||
COURT_FILTER: dict[str, tuple[str, ...]] = {
|
||||
"bverfg": ("BVerfG", "Bundesverfassungsgericht"),
|
||||
"bgh": ("BGH", "Bundesgerichtshof"),
|
||||
"bverwg": ("BVerwG", "Bundesverwaltungsgericht"),
|
||||
"bfh": ("BFH", "Bundesfinanzhof"),
|
||||
"bag": ("BAG", "Bundesarbeitsgericht"),
|
||||
"bsg": ("BSG", "Bundessozialgericht"),
|
||||
"bpatg": ("BPatG", "Bundespatentgericht"),
|
||||
}
|
||||
|
||||
|
||||
def load_toc(url: str = TOC_URL) -> list[dict]:
|
||||
print(f"Lade Inhaltsverzeichnis: {url}")
|
||||
resp = SESSION.get(url, timeout=120)
|
||||
resp.raise_for_status()
|
||||
root = ET.fromstring(resp.content)
|
||||
items: list[dict] = []
|
||||
for item in root.findall("item"):
|
||||
def t(tag: str) -> str:
|
||||
el = item.find(tag)
|
||||
return (el.text or "").strip() if el is not None else ""
|
||||
|
||||
gericht = t("gericht")
|
||||
link = t("link")
|
||||
az = t("aktenzeichen")
|
||||
datum = iso_date(t("entsch-datum"))
|
||||
# doknr manchmal im Link / Titel
|
||||
doknr = t("doknr")
|
||||
if not doknr and link:
|
||||
m = re.search(r"/([A-Z]{4}\d+)", link)
|
||||
if m:
|
||||
doknr = m.group(1)
|
||||
if not link:
|
||||
continue
|
||||
items.append(
|
||||
{
|
||||
"gericht": gericht,
|
||||
"aktenzeichen": az,
|
||||
"entsch-datum": datum,
|
||||
"link": link,
|
||||
"doknr": doknr,
|
||||
"title": t("title") or t("titel"),
|
||||
}
|
||||
)
|
||||
print(f" {len(items)} Einträge")
|
||||
return items
|
||||
|
||||
|
||||
def match_court(gericht: str, wanted: set[str]) -> bool:
|
||||
if not wanted:
|
||||
return True
|
||||
g = gericht or ""
|
||||
for key in wanted:
|
||||
aliases = COURT_FILTER.get(key, (key,))
|
||||
for a in aliases:
|
||||
if a.lower() in g.lower() or g.lower() in a.lower():
|
||||
return True
|
||||
# exakter Typ
|
||||
if g.strip().upper() == a.upper():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def court_keys_from_arg(raw: str | None) -> set[str]:
|
||||
if not raw:
|
||||
return set()
|
||||
keys = set()
|
||||
for part in raw.split(","):
|
||||
k = part.strip().lower()
|
||||
if not k:
|
||||
continue
|
||||
if k in COURT_FILTER:
|
||||
keys.add(k)
|
||||
elif k.upper() in COURT_MAP:
|
||||
keys.add(COURT_MAP[k.upper()][1])
|
||||
else:
|
||||
keys.add(k)
|
||||
return keys
|
||||
|
||||
|
||||
def existing_doknrs(ge_md: Path) -> set[str]:
|
||||
found: set[str] = set()
|
||||
if not ge_md.is_dir():
|
||||
return found
|
||||
for path in ge_md.rglob("*.md"):
|
||||
try:
|
||||
head = path.read_text(encoding="utf-8", errors="replace")[:1200]
|
||||
except OSError:
|
||||
continue
|
||||
m = re.search(r'doknr:\s*"([^"]+)"', head)
|
||||
if m:
|
||||
found.add(m.group(1))
|
||||
continue
|
||||
m = re.search(r"doknr:\s*(\S+)", head)
|
||||
if m:
|
||||
found.add(m.group(1).strip('"'))
|
||||
return found
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="RII-Entscheidungen extrahieren")
|
||||
ap.add_argument("--toc", default=TOC_URL, help="URL des Inhaltsverzeichnisses")
|
||||
ap.add_argument(
|
||||
"--court",
|
||||
default="",
|
||||
help="Kommagetrennt: bgh,bverfg,bverwg,bfh,bag,bsg,bpatg",
|
||||
)
|
||||
ap.add_argument("--since", type=int, default=0, help="Nur ab Entscheidungsjahr")
|
||||
ap.add_argument("--limit", type=int, default=0, help="Max. neu geschriebene Dateien")
|
||||
ap.add_argument("--delay", type=float, default=0.4, help="Pause zwischen Downloads (s)")
|
||||
ap.add_argument("--ge-md", default="ge_md")
|
||||
ap.add_argument("--ge-xml", default="ge_xml")
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
ge_md = Path(args.ge_md)
|
||||
ge_xml = Path(args.ge_xml)
|
||||
wanted = court_keys_from_arg(args.court)
|
||||
|
||||
items = load_toc(args.toc)
|
||||
known = existing_doknrs(ge_md)
|
||||
print(f" bereits in ge_md: {len(known)} doknr")
|
||||
|
||||
work: list[dict] = []
|
||||
skipped_year = 0
|
||||
skipped_court = 0
|
||||
skipped_known = 0
|
||||
by_court: dict[str, list[dict]] = {k: [] for k in wanted} if wanted else {"_all": []}
|
||||
|
||||
for it in items:
|
||||
if not match_court(it["gericht"], wanted):
|
||||
skipped_court += 1
|
||||
continue
|
||||
year_s = (it["entsch-datum"] or "")[:4]
|
||||
if args.since and year_s.isdigit() and int(year_s) < args.since:
|
||||
skipped_year += 1
|
||||
continue
|
||||
if it["doknr"] and it["doknr"] in known:
|
||||
skipped_known += 1
|
||||
continue
|
||||
if wanted:
|
||||
bucket = next(
|
||||
(k for k in wanted if match_court(it["gericht"], {k})),
|
||||
None,
|
||||
)
|
||||
if bucket is None:
|
||||
continue
|
||||
by_court[bucket].append(it)
|
||||
else:
|
||||
by_court["_all"].append(it)
|
||||
|
||||
# Bei --limit: Gerichte round-robin mischen, damit z. B. bgh,bverfg beide vorkommen
|
||||
if args.limit and wanted:
|
||||
per = max(1, (args.limit + len(wanted) - 1) // len(wanted))
|
||||
queues = {k: v[: per * 2] for k, v in by_court.items()}
|
||||
while any(queues.values()) and len(work) < args.limit * 2:
|
||||
for k in list(queues.keys()):
|
||||
if queues[k]:
|
||||
work.append(queues[k].pop(0))
|
||||
if len(work) >= args.limit * 2:
|
||||
break
|
||||
else:
|
||||
for v in by_court.values():
|
||||
work.extend(v)
|
||||
|
||||
print(
|
||||
f" Filter: Gericht übersprungen={skipped_court}, "
|
||||
f"Jahr={skipped_year}, bekannt={skipped_known}, Kandidaten={len(work)}"
|
||||
)
|
||||
if wanted:
|
||||
print(
|
||||
" je Gericht: "
|
||||
+ ", ".join(f"{k}={len(by_court[k])}" for k in sorted(by_court))
|
||||
)
|
||||
|
||||
if args.dry_run:
|
||||
for it in work[: args.limit or 20]:
|
||||
print(f" would fetch: {it['gericht']} {it['aktenzeichen']} {it['link']}")
|
||||
return 0
|
||||
|
||||
written = 0
|
||||
failed = 0
|
||||
limit = args.limit or 10**9
|
||||
|
||||
for i, it in enumerate(work):
|
||||
if written >= limit:
|
||||
break
|
||||
link = it["link"]
|
||||
try:
|
||||
resp = SESSION.get(link, timeout=60)
|
||||
resp.raise_for_status()
|
||||
blob = resp.content
|
||||
except requests.RequestException as e:
|
||||
print(f" Fehler {link}: {e}", file=sys.stderr)
|
||||
failed += 1
|
||||
time.sleep(args.delay)
|
||||
continue
|
||||
|
||||
is_zip = link.endswith(".zip") or blob[:2] == b"PK"
|
||||
if is_zip:
|
||||
try:
|
||||
with zipfile.ZipFile(io.BytesIO(blob)) as zf:
|
||||
names = [n for n in zf.namelist() if n.endswith(".xml")]
|
||||
if not names:
|
||||
failed += 1
|
||||
time.sleep(args.delay)
|
||||
continue
|
||||
xml_bytes = zf.read(names[0])
|
||||
except zipfile.BadZipFile:
|
||||
failed += 1
|
||||
time.sleep(args.delay)
|
||||
continue
|
||||
else:
|
||||
xml_bytes = blob
|
||||
|
||||
parsed = parse_rii_xml(xml_bytes)
|
||||
if not parsed:
|
||||
failed += 1
|
||||
time.sleep(args.delay)
|
||||
continue
|
||||
|
||||
doknr = parsed["meta"].get("doknr") or ""
|
||||
if doknr and doknr in known:
|
||||
skipped_known += 1
|
||||
time.sleep(args.delay)
|
||||
continue
|
||||
|
||||
# Zielpfad schon da?
|
||||
rel = relative_path(parsed["meta"])
|
||||
target = ge_md / rel
|
||||
if target.is_file():
|
||||
known.add(doknr)
|
||||
time.sleep(args.delay)
|
||||
continue
|
||||
|
||||
out = convert_xml_to_markdown_file(xml_bytes, ge_md)
|
||||
if out is None:
|
||||
failed += 1
|
||||
time.sleep(args.delay)
|
||||
continue
|
||||
|
||||
# Roh speichern
|
||||
try:
|
||||
gertyp = parsed["meta"].get("gericht") or "unbekannt"
|
||||
court = COURT_MAP.get(gertyp, ("bund", gertyp.lower()))[1]
|
||||
dest_dir = ge_xml / court
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
raw_name = doknr or out.stem
|
||||
if is_zip:
|
||||
(dest_dir / f"{raw_name}.zip").write_bytes(blob)
|
||||
else:
|
||||
(dest_dir / f"{raw_name}.xml").write_bytes(xml_bytes)
|
||||
except OSError as e:
|
||||
print(f" Warnung Rohspeicher: {e}", file=sys.stderr)
|
||||
|
||||
if doknr:
|
||||
known.add(doknr)
|
||||
written += 1
|
||||
if written % 50 == 0 or written <= 5:
|
||||
print(f" geschrieben {written}: {out}")
|
||||
|
||||
time.sleep(args.delay)
|
||||
|
||||
print(f"Fertig. neu={written}, Fehler={failed}")
|
||||
return 0 if failed == 0 or written > 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
0
ge_md/.gitkeep
Normal file
0
ge_md/.gitkeep
Normal file
27
ge_md/bgh/2010/2-ars-569-09.md
Normal file
27
ge_md/bgh/2010/2-ars-569-09.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
ebene: "bund"
|
||||
gericht: "BGH"
|
||||
spruchkoerper: "2. Strafsenat"
|
||||
datum: "2010-01-13"
|
||||
aktenzeichen: "2 ARs 569/09"
|
||||
doknr: "JURE100055051"
|
||||
doktyp: "Beschluss"
|
||||
quelle: "rii"
|
||||
normen:
|
||||
- { gesetz: "jgg", paragraph: "42", roh: "§ 42 Abs 3 JGG" }
|
||||
vorinstanz: []
|
||||
---
|
||||
|
||||
# BGH Beschluss — 2 ARs 569/09 vom 13.01.2010
|
||||
|
||||
## Titelzeile
|
||||
|
||||
Jugendgerichtsverfahren: Abgabe an das nach Wohnsitzwechsel des Angeklagten zuständige Gericht
|
||||
|
||||
## Tenor
|
||||
|
||||
Für die Untersuchung und Entscheidung der Sache ist das Amtsgericht - Jugendrichter - Nürnberg zuständig.
|
||||
|
||||
## Gründe
|
||||
|
||||
1Die Abgabe durch das Amtsgericht Darmstadt gemäß § 42 Abs. 3 JGG ist zulässig, weil der Angeklagte seinen Wohnsitz Anfang September 2009 und damit nach der Erhebung der Anklage nach Nürnberg verlegt hat (vgl. BGHSt 13, 209, 217). Sie ist auch im Hinblick auf die am Verfahren zu beteiligende Jugendgerichtshilfe des neuen Wohnortes zweckmäßig (vgl. BGH StraFo 2007, 162). Demgegenüber kommt hier dem Umstand, dass Zeugen in Hessen wohnhaft sind, nur eine untergeordnete Bedeutung zu (vgl. Senat, Beschluss vom 7. Februar 2007 - 2 ARs 547/06).Rissing-van Saan Fischer ApplCierniak Krehl
|
||||
29
ge_md/bgh/2010/2-str-428-09.md
Normal file
29
ge_md/bgh/2010/2-str-428-09.md
Normal file
File diff suppressed because one or more lines are too long
30
ge_md/bgh/2010/2-str-447-09.md
Normal file
30
ge_md/bgh/2010/2-str-447-09.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
ebene: "bund"
|
||||
gericht: "BGH"
|
||||
spruchkoerper: "2. Strafsenat"
|
||||
datum: "2010-01-13"
|
||||
aktenzeichen: "2 StR 447/09"
|
||||
doknr: "JURE100055074"
|
||||
doktyp: "Beschluss"
|
||||
quelle: "rii"
|
||||
normen:
|
||||
- { gesetz: "stgb", paragraph: "20", roh: "§ 20 StGB" }
|
||||
- { gesetz: "stgb", paragraph: "224", roh: "§ 224 StGB" }
|
||||
- { gesetz: "stgb", paragraph: "241", roh: "§ 241 StGB" }
|
||||
vorinstanz:
|
||||
- "vorgehend LG Mühlhausen, 24. Juni 2009, Az: 120 Js 58726/08 - 1 Ks, Urteil"
|
||||
---
|
||||
|
||||
# BGH Beschluss — 2 StR 447/09 vom 13.01.2010
|
||||
|
||||
## Titelzeile
|
||||
|
||||
Strafverfahren wegen Bedrohung und gefährlicher Körperverletzung: Prüfung der Schuldunfähigkeit bei hoher Blutalkoholkonzentration und nicht nachvollziehbarem Tatmotiv
|
||||
|
||||
## Tenor
|
||||
|
||||
Auf die Revision des Angeklagten wird das Urteil des Landgerichts Mühlhausen vom 24. Juni 2009 mit den Feststellungen aufgehoben.Die Sache wird zu neuer Verhandlung und Entscheidung, auch über die Kosten des Rechtsmittels, an eine andere Strafkammer des Landgerichts zurückverwiesen.
|
||||
|
||||
## Gründe
|
||||
|
||||
1Das Landgericht hat den Angeklagten wegen Bedrohung und gefährlicher Körperverletzung zu einer Gesamtfreiheitsstrafe von drei Jahren verurteilt. Die Revision des Angeklagten hat mit der allgemeinen Sachrüge Erfolg.21. Nach den Feststellungen spielten der Angeklagte, das spätere Tatopfer und ein Dritter am Tattag seit dem frühen Morgen im Haus des Geschädigten Karten und konsumierten dabei erhebliche Mengen Alkohol. Gegen 16.30 Uhr fütterte der Angeklagte eine fremde Katze aus der Nachbarschaft, worüber der Geschädigte seinen Unmut zum Ausdruck brachte. Daraufhin verließ der Angeklagte das Haus, holte aus dem Hof eine Axt und äußerte ins Wohnzimmer zurückkehrend gegenüber seinen Mitspielern, er werde sie totschlagen. Anschließend griff er, die Axt schwingend, den Geschädigten an, dem - von zwei Schlägen nur gestreift - leicht verletzt die Flucht zu einem Nachbarhaus gelang, von wo aus er die Polizei verständigte. Währenddessen setzte sich der Angeklagte wortlos zu dem dritten im Wohnzimmer verbliebenen Spieler und wartete auf das Eintreffen der Polizei. Eine ihm um 18.00 Uhr entnommene Blutprobe ergab eine BAK von 2,91 Promille.32. Das Landgericht ist sachverständig beraten davon ausgegangen, der Angeklagte sei infolge des zuvor genossenen Alkohols aufgrund eines mittelgradigen Rausches nur erheblich vermindert in der Lage gewesen, sein Verhalten zu steuern. Mit der Frage einer möglichen Schuldunfähigkeit gemäß § 20 StGB hat sich die Strafkammer nicht auseinandergesetzt. Dies begegnet durchgreifenden rechtlichen Bedenken. Rückgerechnet auf den Tatzeitpunkt wies der Angeklagte - was das Landgericht nicht bedacht hat - eine BAK von 3,41 Promille auf. Bei einer solch hohen Blutalkoholkonzentration ist regelmäßig die Prüfung einer Aufhebung der Schuldfähigkeit veranlasst (Fischer, StGB 57. Aufl. § 20 Rdn. 19 ff. m.w.N.). Zu einer solchen Prüfung bestand hier auch Veranlassung im Hinblick auf das kaum nachvollziehbare Tatmotiv, das ungewöhnliche Nachtatverhalten und angesichts des Umstandes, dass der nicht vorbestrafte Angeklagte weder generell zu Wut- und Aggressionsausbrüchen neigt noch sonst für aggressive Entgleisungen bekannt ist (UA 9).43. Die aufgezeigte Lückenhaftigkeit des Urteils führt zu dessen Aufhebung und Zurückverweisung. Der neue Tatrichter wird - sollte er erneut zu einer entsprechenden Verurteilung kommen - zu bedenken haben, dass bei Verhängung von einzelnen Geldstrafen, die in einer Gesamtstrafe aufgehen, neben der Anzahl der Tagessätze auch immer die Tagessatzhöhe festzusetzen ist (BGHSt 30, 93, 96; BGHR StGB § 54 Abs. 3 Tagessatzhöhe 1).Rissing-van Saan Fischer ApplCierniak Krehl
|
||||
30
ge_md/bgh/2010/ix-zb-72-08.md
Normal file
30
ge_md/bgh/2010/ix-zb-72-08.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
ebene: "bund"
|
||||
gericht: "BGH"
|
||||
spruchkoerper: "9. Zivilsenat"
|
||||
datum: "2010-01-14"
|
||||
aktenzeichen: "IX ZB 72/08"
|
||||
doknr: "JURE100055033"
|
||||
doktyp: "Beschluss"
|
||||
quelle: "rii"
|
||||
normen:
|
||||
- { gesetz: "inso", paragraph: "4", roh: "§ 4 InsO" }
|
||||
- { gesetz: "zpo", paragraph: "13", roh: "§ 13 ZPO" }
|
||||
- { gesetz: "zpo", paragraph: "251", roh: "§ 251 ZPO" }
|
||||
vorinstanz:
|
||||
- "vorgehend LG Aachen, 17. März 2008, Az: 6 T 104/07, Beschlussvorgehend AG Aachen, 13. Februar 2007, Az: 92 IN 9/07"
|
||||
---
|
||||
|
||||
# BGH Beschluss — IX ZB 72/08 vom 14.01.2010
|
||||
|
||||
## Titelzeile
|
||||
|
||||
Insolvenzverfahren: Anwendbarkeit der Vorschrift über das Ruhen des Verfahrens; Beschwerderecht des Insolvenzverwalters im Zusammenhang mit der Verfahrenseröffnung
|
||||
|
||||
## Tenor
|
||||
|
||||
Der Antrag des weiteren Beteiligten zu 2 auf Bewilligung von Prozesskostenhilfe wird abgelehnt.Die Rechtsbeschwerde gegen den Beschluss der 6. Zivilkammer des Landgerichts Aachen vom 17. März 2008 wird auf Kosten der Schuldnerin als unzulässig verworfen.Der Gegenstandswert des Rechtsbeschwerdeverfahrens wird auf 50.000 € festgesetzt.
|
||||
|
||||
## Gründe
|
||||
|
||||
11. Einer Entscheidung steht nicht entgegen, dass die Schuldnerin das Ruhen des Verfahrens beantragt hat. Die Vorschrift über das Ruhen des Verfahrens (§ 251 ZPO) ist im grundsätzlich eilbedürftigen, auf eine rasche Befriedigung der Gläubiger angelegten Insolvenzverfahren und damit auch im Verfahren über eine insolvenzrechtliche Rechtsbeschwerde nicht anwendbar (MünchKomm-InsO/Ganter, 2. Aufl. § 4 Rn. 15; Jaeger/Gerhardt, InsO § 4 Rn. 57; HK-InsO/Kirchhof, 5. Aufl. § 4 Rn. 25; HmbKomm-InsO/Rüther, 3. Aufl. § 4 Rn. 57). Im Übrigen hat die weitere Beteiligte zu 1, die als Gläubigerin die Eröffnung des Insolvenzverfahrens beantragt hat, als Rechtsbeschwerdegegnerin dem Antrag nicht zugestimmt.22. Dem weiteren Beteiligten zu 2 kann nach § 4 InsO in Verbindung mit §§ 114 ff ZPO keine Prozesskostenhilfe gewährt werden, weil er im Rechtsbeschwerdeverfahren gegen die Eröffnung des Insolvenzverfahrens keine eigenen Rechte verfolgen kann. Im Ausgangspunkt kann Prozesskostenhilfe nur der "Partei" gewährt werden (§ 114 Abs. 1 Satz 1 ZPO). Dieser Begriff ist zwar weit auszulegen (vgl. Musielak/Fischer, ZPO 7. Aufl. § 114 Rn. 2); es ist deshalb anerkannt, dass die Vorschrift auch Antragsteller, Antragsgegner sowie die Streithelfer der Parteien erfasst. Der weitere Beteiligte zu 2 gehört im Streitfall als Insolvenzverwalter indes nicht zu diesem Personenkreis (vgl. MünchKomm-InsO/Ganter, aaO § 4 Rn. 21; Jaeger/Gerhardt, aaO § 4 Rn. 48). Er ist nicht berechtigt, die Eröffnung des Insolvenzverfahrens zu beantragen (§ 13 Abs. 1 InsO). Auch ein Beschwerderecht räumt ihm die Insolvenzordnung im Zusammenhang mit der Verfahrenseröffnung nicht ein (BGH, Beschl. v. 8. März 2007 - IX ZB 163/06, ZIP 2007, 792).33. Die Rechtsbeschwerde ist statthaft (§§ 7, 6, 34 Abs. 2 Satz 1 InsO, § 574 Abs. 1 Satz 1 Nr. 1 ZPO), aber unzulässig. Weder hat die Rechtssache grundsätzliche Bedeutung noch erfordert die Fortbildung des Rechts oder die Sicherung einer einheitlichen Rechtsprechung eine Entscheidung des Rechtsbeschwerdegerichts (§ 574 Abs. 2 ZPO).4a) Die von der Rechtsbeschwerde für grundsätzlich erachtete Frage, ob eine nach dem Ausscheiden ihres vorletzten Gesellschafters erloschene, aber noch im Handelsregister eingetragene Kommanditgesellschaft nach dem Rechtsgedanken des § 15 HGB weiterhin insolvenzfähig ist, bedarf im Streitfall keiner Entscheidung, weil das Beschwerdegericht den Austritt der Gesellschafter aus der KG ohne zulassungsrelevanten Rechtsfehler als unwirksam nach § 117 BGB beurteilt hat; seine Rechtsauffassung zu § 15 HGB war daher nicht entscheidungserheblich.5b) Auch die Ausführungen des Beschwerdegerichts zum rechtlichen Interesse der weiteren Beteiligten zu 1 an der Eröffnung des Insolvenzverfahrens (§ 14 Abs. 1 InsO) begründen nicht die Zulässigkeit der Rechtsbeschwerde. Dass die Vorinstanzen den zugrunde liegenden Sachverhalt insoweit anders subsumiert haben als der zuständige Abteilungsrichter im Insolvenzverfahren betreffend das Vermögen des L. N., berührt nicht die Einheitlichkeit der Rechtsprechung i.S.v. § 574 Abs. 2 Nr. 2 ZPO (BGH, Beschl. v. 16. September 2003 - XI ZR 238/02, NJW 2004, 1167).6c) Dahinstehen kann schließlich, ob das Beschwerdegericht die Forderung der U. GmbH gegen die Schuldnerin bei der Beurteilung des Eröffnungsgrundes der Zahlungsunfähigkeit trotz der Erklärung des "Generalbevollmächtigten" der U. GmbH betreffend eine angebliche Stundung dieser Forderung berücksichtigen durfte. Denn die Beurteilung des Beschwerdegerichts zur Zahlungsunfähigkeit der Schuldnerin wird bereits durch die unstreitig bestehende Umsatzsteuerforderung des Finanzamts G. getragen. Die von der Rechtsbeschwerde insoweit gerügte Verletzung des rechtlichen Gehörs (Art. 103 Abs. 1 GG) liegt nicht vor.Ganter Gehrlein VillFischer Grupp
|
||||
31
ge_md/bgh/2010/ix-zb-76-09.md
Normal file
31
ge_md/bgh/2010/ix-zb-76-09.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
ebene: "bund"
|
||||
gericht: "BGH"
|
||||
spruchkoerper: "9. Zivilsenat"
|
||||
datum: "2010-01-14"
|
||||
aktenzeichen: "IX ZB 76/09"
|
||||
doknr: "JURE100055035"
|
||||
doktyp: "Beschluss"
|
||||
quelle: "rii"
|
||||
normen:
|
||||
- { gesetz: "inso", paragraph: "315", roh: "§ 315 InsO" }
|
||||
- { gesetz: "zpo", paragraph: "13", roh: "§ 13 ZPO" }
|
||||
- { gesetz: "zpo", paragraph: "16", roh: "§ 16 ZPO" }
|
||||
- { gesetz: "", paragraph: "", roh: "§ 3 EGV 1346/2000" }
|
||||
vorinstanz:
|
||||
- "vorgehend LG München I, 14. Februar 2009, Az: 14 T 352/09, Beschlussvorgehend AG München, 2. Oktober 2008, Az: 1542 IN 2678/08"
|
||||
---
|
||||
|
||||
# BGH Beschluss — IX ZB 76/09 vom 14.01.2010
|
||||
|
||||
## Titelzeile
|
||||
|
||||
Nachlassinsolvenzverfahren: Örtliche Zuständigkeit des Insolvenzgerichts; grenzüberschreitende Insolvenz; wohnsitzlose Person als Erblasser
|
||||
|
||||
## Tenor
|
||||
|
||||
Die Rechtsbeschwerde gegen den Beschluss der 14. Zivilkammer des Landgerichts München I vom 14. Februar 2009 wird auf Kosten des Antragstellers als unzulässig verworfen.Der Gegenstandswert des Rechtsbeschwerdeverfahrens wird auf 50.000 € festgesetzt.
|
||||
|
||||
## Gründe
|
||||
|
||||
1Die Rechtsbeschwerde ist statthaft (§ 574 Abs. 1 Satz 1 Nr. 1 ZPO in Verbindung mit §§ 6, 7, 34 Abs. 1 InsO), jedoch unzulässig. Die nach § 574 Abs. 2 ZPO geltend gemachten Zulässigkeitsgründe liegen nicht vor; weder hat die Rechtssache grundsätzliche Bedeutung noch erfordert die Sicherung einer einheitlichen Rechtsprechung eine Entscheidung des Rechtsbeschwerdegerichts.21. Die Frage der örtlichen Zuständigkeit des Amtsgerichts München für die Eröffnung des Nachlassinsolvenzverfahrens wäre schon nicht entscheidungserheblich, wenn die internationale Zuständigkeit der deutschen Gerichte nicht gegeben wäre. Wäre § 3 EuInsVO für das Nachlassinsolvenzverfahren anwendbar (nach wohl herrschender Meinung ist dies zu bejahen; vgl. HK-InsO/Marotzke, 5. Aufl. § 315 Rn. 8 m.w.N.), schiede die internationale Zuständigkeit der deutschen Gerichte aus, weil der Schuldner den Mittelpunkt seiner hauptsächlichen Interessen in Spanien hatte. Ein Zulässigkeitsgrund wird insoweit nicht dargelegt.32. Ist § 3 EuInsVO nicht anwendbar, wird für die internationale und örtliche Zuständigkeit § 315 InsO maßgebend. Ausschließlich zuständig ist danach das Insolvenzgericht, in dessen Bezirk der Erblasser zur Zeit seines Todes seinen allgemeinen Gerichtsstand hatte. Dies richtet sich gemäß § 13 ZPO nach dem Wohnsitz. Bei wohnsitzlosen Personen ist gemäß § 16 ZPO allgemeiner Gerichtsstand der Aufenthaltsort im Inland, wenn ein solcher nicht bekannt ist der Ort des letzten Wohnsitzes. Hat die Person einen Wohnsitz im Ausland, ist dagegen § 16 ZPO nicht anwendbar, und es besteht keine inländische Zuständigkeit (MünchKomm-InsO/Siegmann, 2. Aufl. § 315 Rn. 8; MünchKomm-InsO/Ganter, aaO § 3 Rn. 17). Das Beschwerdegericht hat einen Wohnsitz im Ausland bejaht.4Die Frage, wann jemand wohnsitzlos im Sinne des § 16 ZPO ist, hat keine grundsätzliche Bedeutung. Es ist geklärt, wann ein Wohnsitz vorliegt. Der Wohnsitz wird gemäß § 7 Abs. 1 BGB dort begründet, wo sich eine Person ständig niederlässt. Erforderlich ist eine tatsächliche Niederlassung mit dem Willen, den Ort zum ständigen Schwerpunkt der Lebensverhältnisse zu machen. Die Niederlassung erfordert eine eigene Unterkunft, ein Obdachloser hat keinen Wohnsitz. Es genügt jedoch eine behelfsmäßige Unterkunft. Der Domizilwille muss darauf gerichtet sein, den Ort zum ständigen Schwerpunkt der Lebensverhältnisse zu machen. Dies kann sich aus den Umständen ergeben. Beides war hier der Fall. Es ist nicht erkennbar, dass das Landgericht den Begriff verkannt hätte. Es hat auch nicht Obdachlosigkeit mit Wohnsitzlosigkeit verwechselt, sondern lediglich angedeutet, dass Obdachlose keinen Wohnsitz haben.5Aus der öffentlichen Zustellung des vom Antragsteller erwirkten Versäumnisurteils ergibt sich schon deshalb nichts anderes, weil völlig offen ist, ob damals die hierfür erforderlichen Voraussetzungen tatsächlich vorgelegen haben und sich zwischenzeitlich auch nichts geändert hat.63. Eine Verletzung des rechtlichen Gehörs des Antragstellers liegt nicht darin, dass das Beschwerdegericht kein Partikularinsolvenzverfahren eröffnet hat. Die Eröffnung eines solchen Verfahrens war nicht - auch nicht hilfsweise - beantragt worden. Dessen Voraussetzungen hatte der Gläubiger auch nicht dargelegt, sein Interesse hieran nicht glaubhaft gemacht. Das Vorliegen inländischen Vermögens wurde nicht substantiiert dargelegt. Die Grundstücke liegen in Spanien und stehen im Eigentum spanischer Gesellschaften. Ob auf die Gesellschaftsanteile des Schuldners zugegriffen werden kann und ob sie werthaltig sind, ist unklar. Lediglich eine geschäftsführend tätige Gesellschaft soll ihren Sitz in B. S. haben.7Eine örtliche Zuständigkeit des Amtsgerichts München besteht hierfür jedenfalls nicht (vgl. § 354 Abs. 3 InsO; Art. 102 § 1 Abs. 2 und 3 EGInsO). Der Gläubiger hatte auch keinen Verweisungsantrag gemäß § 281 ZPO, § 4 InsO gestellt.Ganter Gehrlein VillLohmann Fischer
|
||||
32
ge_md/bverfg/2024/1-bvr-2231-23.md
Normal file
32
ge_md/bverfg/2024/1-bvr-2231-23.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
ebene: "verfg"
|
||||
gericht: "BVerfG"
|
||||
spruchkoerper: "1. Senat 3. Kammer"
|
||||
datum: "2024-07-02"
|
||||
aktenzeichen: "1 BvR 2231/23"
|
||||
ecli: "ECLI:DE:BVerfG:2024:rk20240702.1bvr223123"
|
||||
doknr: "KVRE000022442"
|
||||
doktyp: "Nichtannahmebeschluss"
|
||||
quelle: "rii"
|
||||
normen:
|
||||
- { gesetz: "gg", paragraph: "4", roh: "Art 4 Abs 1 GG" }
|
||||
- { gesetz: "gg", paragraph: "4", roh: "Art 4 Abs 2 GG" }
|
||||
- { gesetz: "bverfgg", paragraph: "23", roh: "§ 23 Abs 1 S 2 BVerfGG" }
|
||||
- { gesetz: "bverfgg", paragraph: "92", roh: "§ 92 BVerfGG" }
|
||||
vorinstanz:
|
||||
- "vorgehend BAG, 25. April 2023, Az: 9 AZR 254/22, Urteil"
|
||||
---
|
||||
|
||||
# BVerfG Nichtannahmebeschluss — 1 BvR 2231/23 vom 02.07.2024
|
||||
|
||||
## Titelzeile
|
||||
|
||||
Nichtannahmebeschluss: Erfolglose Verfassungsbeschwerde bzgl der Arbeitnehmereigenschaft von Sevaka-Mitgliedern eines Yoga- und Meditationszentrums - religiöse Prägung der Dienstleistung weder dargelegt noch ersichtlich
|
||||
|
||||
## Tenor
|
||||
|
||||
Die Verfassungsbeschwerde wird nicht zur Entscheidung angenommen, weil sie den gesetzlichen Darlegungsanforderungen nicht gerecht wird (§ 23 Abs. 1 Satz 2, § 92 BVerfGG) und es an einem Annahmegrund fehlt (§ 93a Abs. 1 BVerfGG, vgl. BVerfGE 111, 1 <4 f.>). Es kann dahinstehen, ob die Annahme des Bundesarbeitsgerichts, bei dem Beschwerdeführer, einem „(…)“-Verein, handele es sich nicht um eine Religions- oder Weltanschauungsgemeinschaft, mit Artikel 4 Absatz 1 und 2 Grundgesetz vereinbar ist. Denn es ist weder dargelegt noch sonst ersichtlich, dass die von der Klägerin des Ausgangsverfahrens geleisteten Dienste der Aufrechterhaltung des Beherbergungs- und Seminarbetriebs des Vereins und des Vertriebs von Yoga-Produkten, um deren arbeitsrechtliche Beurteilung es hier geht, für sich genommen religiös geprägt waren.Von einer weiteren Begründung der Nichtannahme der Verfassungsbeschwerde zur Entscheidung wird nach § 93d Abs. 1 Satz 3 BVerfGG abgesehen.
|
||||
|
||||
## Gründe
|
||||
|
||||
Diese Entscheidung ist unanfechtbar.
|
||||
31
ge_md/bverfg/2024/1-bvr-2268-23.md
Normal file
31
ge_md/bverfg/2024/1-bvr-2268-23.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
ebene: "verfg"
|
||||
gericht: "BVerfG"
|
||||
spruchkoerper: "1. Senat 2. Kammer"
|
||||
datum: "2024-11-20"
|
||||
aktenzeichen: "1 BvR 2268/23"
|
||||
ecli: "ECLI:DE:BVerfG:2024:rk20241120.1bvr226823"
|
||||
doknr: "KVRE000012542"
|
||||
doktyp: "Kammerbeschluss"
|
||||
quelle: "rii"
|
||||
normen:
|
||||
- { gesetz: "bverfgg", paragraph: "90", roh: "§ 90 BVerfGG" }
|
||||
- { gesetz: "zpo", paragraph: "251", roh: "§ 251 ZPO" }
|
||||
- { gesetz: "ensig", paragraph: "17", roh: "§ 17 EnSiG" }
|
||||
vorinstanz:
|
||||
- "vorgehend BVerwG, 2. November 2023, Az: 8 A 3/23, Beschlussvorgehend BVerwG, 14. März 2023, Az: 8 A 2/22, Urteil"
|
||||
---
|
||||
|
||||
# BVerfG Kammerbeschluss — 1 BvR 2268/23 vom 20.11.2024
|
||||
|
||||
## Titelzeile
|
||||
|
||||
Anordnung des Ruhens eines Verfassungsbeschwerdeverfahrens
|
||||
|
||||
## Tenor
|
||||
|
||||
Das Ruhen des Verfahrens wird angeordnet.
|
||||
|
||||
## Gründe
|
||||
|
||||
I.1Die Beschwerdeführerinnen haben sich mit ihrer Verfassungsbeschwerde gegen die Anordnung einer Treuhandverwaltung gemäß § 17 EnSiG und gegen die Entscheidungen des Bundesverwaltungsgerichts, in denen die Klage der Beschwerdeführerinnen gegen die Anordnung der Treuhandverwaltung sowie deren Anhörungsrüge zurückgewiesen wurden, gewandt.2Mittlerweile haben die Beschwerdeführerinnen das Ruhen des Verfahrens beantragt und Gründe vorgetragen, aus denen sich ergibt, dass sich die Bedeutung des vorliegenden Verfahrens für die Beschwerdeführerinnen in den kommenden Monaten grundlegend verändern könnte. Die Bundesregierung, vertreten durch das Bundesministerium für Wirtschaft und Klimaschutz, befürwortet die Anordnung des Ruhens des Verfahrens.II.3Das Ruhen des Verfahrens ist in entsprechender Anwendung von § 251 ZPO anzuordnen (vgl. - ohne Nennung von § 251 ZPO - BVerfGE 89, 327 <328>). Das von den Beschwerdeführerinnen beantragte Ruhen ist zweckmäßig; öffentliche Interessen an der Fortführung des Verfahrens stehen nicht entgegen.
|
||||
30
ge_md/bverfg/2024/1-bvr-875-23.md
Normal file
30
ge_md/bverfg/2024/1-bvr-875-23.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
ebene: "verfg"
|
||||
gericht: "BVerfG"
|
||||
spruchkoerper: "1. Senat 3. Kammer"
|
||||
datum: "2024-06-20"
|
||||
aktenzeichen: "1 BvR 875/23"
|
||||
ecli: "ECLI:DE:BVerfG:2024:rk20240620.1bvr087523"
|
||||
doknr: "KVRE000032442"
|
||||
doktyp: "Prozesskostenhilfebeschluss"
|
||||
quelle: "rii"
|
||||
normen:
|
||||
- { gesetz: "bverfgg", paragraph: "90", roh: "§ 90 BVerfGG" }
|
||||
- { gesetz: "zpo", paragraph: "114", roh: "§ 114 Abs 1 S 1 ZPO" }
|
||||
vorinstanz:
|
||||
- "vorgehend SG Münster, 21. März 2023, Az: S 11 SF 21/23 E RG, Beschlussvorgehend SG Münster, 12. Januar 2023, Az: S 11 SF 66/22 E, Beschlussvorgehend SG Münster, 22. September 2023, Az: S 11 AS 529/14, Kostenfestsetzungsbeschluss"
|
||||
---
|
||||
|
||||
# BVerfG Prozesskostenhilfebeschluss — 1 BvR 875/23 vom 20.06.2024
|
||||
|
||||
## Titelzeile
|
||||
|
||||
Ablehnung eines isolierten PKH-Antrags für eine noch zu erhebende Verfassungsbeschwerde - mangelnde Darlegungen zur Erforderlichkeit der PKH-Gewährung sowie zu den Erfolgsaussichten der Rechtsverfolgung
|
||||
|
||||
## Tenor
|
||||
|
||||
Der Antrag auf Bewilligung von Prozesskostenhilfe und Beiordnung eines Rechtsanwalts für die beabsichtigte Verfassungsbeschwerde gegen den Kostenfestsetzungsbeschluss des Sozialgerichts Münster vom 22. September 2022 - S 11 AS 529/14 -, den Beschluss des Sozialgerichts Münster vom 12. Januar 2023 - S 11 SF 66/22 E - und den Beschluss des Sozialgerichts Münster vom 21. März 2023 - S 11 SF 21/23 E RG - wird abgelehnt.
|
||||
|
||||
## Gründe
|
||||
|
||||
11. Nach der Rechtsprechung des Bundesverfassungsgerichts ist im Verfahren über eine Verfassungsbeschwerde die Bewilligung von Prozesskostenhilfe an den Beschwerdeführer entsprechend § 114 ff. ZPO möglich (vgl. BVerfGE 1, 109 <110 f.>; 1, 415 <416>; 79, 252 <253>; 92, 122 <123>; BVerfG, Beschluss der 2. Kammer des Ersten Senats vom 23. Februar 2022 - 1 BvR 2434/21 -, Rn. 1). Auch die isolierte Bewilligung von PKH für eine noch zu erhebende Verfassungsbeschwerde ist nicht ausgeschlossen. PKH wird aber nur unter strengen Voraussetzungen gewährt, weil das Verfahren kostenfrei ist und kein Anwaltszwang besteht. Sie wird daher nur gewährt, wenn dies unbedingt erforderlich erscheint, weil die betroffene Person nicht in der Lage ist, sich selbst zu vertreten, sie die Kosten der Prozessführung nach den persönlichen und wirtschaftlichen Verhältnissen nicht aufbringen kann und die beabsichtigte Rechtsverfolgung hinreichend Aussicht auf Erfolg bietet (vgl. BVerfGE 78, 7 <19 f.>; 92, 122 <123>; BVerfG, Beschluss der 3. Kammer des Zweiten Senats vom 9. Juli 2010 - 2 BvR 2258/09 -, juris, Rn. 6; Beschluss der 1. Kammer des Ersten Senats vom 2. Dezember 2016 - 1 BvR 2014/16 -, Rn. 2 m.w.N.; Beschluss der 2. Kammer des Zweiten Senats vom 2. März 2020 - 2 BvR 1819/19 -, Rn. 3).22. Diese Voraussetzungen liegen hier nicht vor.3Der Antragsteller legt nicht in einer den Anforderungen des § 23 Abs. 1 Satz 2 Halbsatz 1 BVerfGG entsprechenden Weise dar, dass er daran gehindert ist, seine Rechte selbst und ohne anwaltliche Hilfe angemessen wahrzunehmen.4Überdies bietet die beabsichtigte Verfassungsbeschwerde keine hinreichende Aussicht auf Erfolg, insbesondere weil weder eine Verletzung des Willkürverbots (Art. 3 Abs. 1 GG) noch eine Verletzung des Anspruchs auf rechtliches Gehör (Art. 103 Abs. 1 GG) erkennbar sind.5Diese Entscheidung ist unanfechtbar.
|
||||
31
ge_md/bverfg/2024/2-bvr-923-24.md
Normal file
31
ge_md/bverfg/2024/2-bvr-923-24.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
ebene: "verfg"
|
||||
gericht: "BVerfG"
|
||||
spruchkoerper: "2. Senat 2. Kammer"
|
||||
datum: "2024-07-12"
|
||||
aktenzeichen: "2 BvR 923/24"
|
||||
ecli: "ECLI:DE:BVerfG:2024:rk20240712.2bvr092324"
|
||||
doknr: "KVRE000012442"
|
||||
doktyp: "Ablehnung einstweilige Anordnung"
|
||||
quelle: "rii"
|
||||
normen:
|
||||
- { gesetz: "bverfgg", paragraph: "23", roh: "§ 23 Abs 1 S 2 BVerfGG" }
|
||||
- { gesetz: "bverfgg", paragraph: "32", roh: "§ 32 Abs 1 BVerfGG" }
|
||||
- { gesetz: "bverfgg", paragraph: "92", roh: "§ 92 BVerfGG" }
|
||||
vorinstanz:
|
||||
- "vorgehend VG Potsdam, 10. Juli 2024, Az: VG 14 L 588/24.A, Beschluss"
|
||||
---
|
||||
|
||||
# BVerfG Ablehnung einstweilige Anordnung — 2 BvR 923/24 vom 12.07.2024
|
||||
|
||||
## Titelzeile
|
||||
|
||||
Erfolgloser Eilantrag im Verfassungsbeschwerdeverfahren bei derzeitiger offensichtlicher Unzulässigkeit der Verfassungsbeschwerde
|
||||
|
||||
## Tenor
|
||||
|
||||
Der Antrag auf Erlass einer einstweiligen Anordnung wird abgelehnt. Die Verfassungsbeschwerde erweist sich im gegenwärtigen Zeitpunkt als offensichtlich unzulässig. Für eine Folgenabwägung bleibt daher kein Raum.
|
||||
|
||||
## Gründe
|
||||
|
||||
1Der Antrag auf Erlass einer einstweiligen Anordnung wird abgelehnt, weil die Verfassungsbeschwerde sich nach derzeitiger Aktenlage als offensichtlich unzulässig erweist. Denn der bisherige Vortrag der Antragstellerinnen genügt den Begründungsanforderungen gemäß § 23 Absatz 1 Satz 2 Halbsatz 1, § 92 des Bundesverfassungsgerichtsgesetzes nicht, da eine mögliche Verletzung von Grundrechten nicht hinreichend dargelegt wurde. Für eine Folgenabwägung bleibt daher kein Raum.2Von einer weiteren Begründung wird nach § 93d Abs. 1 Satz 3 BVerfGG abgesehen.3Diese Entscheidung ist unanfechtbar.
|
||||
245
index_normen.py
Normal file
245
index_normen.py
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Baut data/normen_index.json: Gesetz/Paragraph → Liste von Entscheidungen (doknr).
|
||||
|
||||
Liest YAML-Köpfe aus ge_md/, mappt Abkürzungen auf Lawgit-Slugs und prüft,
|
||||
ob laws_md/<slug>/§N.md existiert.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
# Häufige RII-Abkürzungen → Ordnername unter laws_md/
|
||||
GESETZ_ALIASE: dict[str, str] = {
|
||||
"bgb": "bgb",
|
||||
"gg": "gg",
|
||||
"stgb": "stgb",
|
||||
"stpo": "stpo",
|
||||
"zpo": "zpo",
|
||||
"inso": "inso",
|
||||
"hgb": "hgb",
|
||||
"gmbhg": "gmbhg",
|
||||
"aktg": "aktg",
|
||||
"sgb": "sgb", # oft mit Bandnummer — unmapped oft
|
||||
"bverfgg": "bverfgg",
|
||||
"vwgo": "vwgo",
|
||||
"fgo": "fgo",
|
||||
"sgg": "sgg",
|
||||
"arbgg": "arbgg",
|
||||
"gvg": "gvg",
|
||||
"egbgb": "egbgb",
|
||||
"agg": "agg",
|
||||
"urhg": "urhg",
|
||||
"tmg": "tmg",
|
||||
"bdsg": "bdsg",
|
||||
"ao": "ao",
|
||||
"ustg": "ustg",
|
||||
"estg": "estg",
|
||||
"vvg": "vvg",
|
||||
"vob": "vob",
|
||||
"baugb": "baugb",
|
||||
"whg": "whg",
|
||||
"bnatschg": "bnatschg",
|
||||
}
|
||||
|
||||
|
||||
def map_gesetz_slug(abbrev: str) -> str:
|
||||
key = (abbrev or "").strip().lower()
|
||||
key = (
|
||||
key.replace("ä", "ae")
|
||||
.replace("ö", "oe")
|
||||
.replace("ü", "ue")
|
||||
.replace("ß", "ss")
|
||||
.replace(".", "")
|
||||
)
|
||||
return GESETZ_ALIASE.get(key, key)
|
||||
|
||||
|
||||
def lawgit_path_for(
|
||||
lawgit_root: Path, gesetz_slug: str, paragraph: str
|
||||
) -> tuple[Path | None, bool]:
|
||||
"""Pfad zu laws_md/<slug>/§N.md (oder Art. N.md) und ob die Datei existiert."""
|
||||
if not gesetz_slug:
|
||||
return None, False
|
||||
slug = map_gesetz_slug(gesetz_slug)
|
||||
base = lawgit_root / "laws_md" / slug
|
||||
if not paragraph:
|
||||
readme = base / "README.md"
|
||||
return readme, readme.is_file()
|
||||
|
||||
candidates = [
|
||||
base / f"§{paragraph}.md",
|
||||
base / f"§{paragraph.lower()}.md",
|
||||
base / f"Art. {paragraph}.md",
|
||||
base / f"Art. {paragraph.lower()}.md",
|
||||
base / f"Art {paragraph}.md",
|
||||
base / f"Artikel {paragraph}.md",
|
||||
]
|
||||
for path in candidates:
|
||||
if path.is_file():
|
||||
return path, True
|
||||
return candidates[0], False
|
||||
|
||||
|
||||
def parse_frontmatter(md: str) -> dict:
|
||||
"""Einfacher YAML-Kopf-Parser für unsere festgelegten Felder."""
|
||||
if not md.startswith("---"):
|
||||
return {}
|
||||
end = md.find("\n---", 3)
|
||||
if end < 0:
|
||||
return {}
|
||||
block = md[3:end].strip()
|
||||
meta: dict = {"normen": [], "vorinstanz": []}
|
||||
current_list: str | None = None
|
||||
for line in block.splitlines():
|
||||
if line.startswith("normen:"):
|
||||
current_list = "normen"
|
||||
if line.strip() == "normen: []":
|
||||
current_list = None
|
||||
continue
|
||||
if line.startswith("vorinstanz:"):
|
||||
current_list = "vorinstanz"
|
||||
if line.strip() == "vorinstanz: []":
|
||||
current_list = None
|
||||
continue
|
||||
if line.startswith(" - ") and current_list == "normen":
|
||||
# { gesetz: "...", paragraph: "...", roh: "..." }
|
||||
m = re.search(
|
||||
r'gesetz:\s*"([^"]*)".*?paragraph:\s*"([^"]*)".*?roh:\s*"([^"]*)"',
|
||||
line,
|
||||
)
|
||||
if m:
|
||||
meta["normen"].append(
|
||||
{"gesetz": m.group(1), "paragraph": m.group(2), "roh": m.group(3)}
|
||||
)
|
||||
continue
|
||||
if line.startswith(" - ") and current_list == "vorinstanz":
|
||||
continue
|
||||
if re.match(r"^[a-z_]+:", line) and not line.startswith(" "):
|
||||
current_list = None
|
||||
key, _, val = line.partition(":")
|
||||
val = val.strip()
|
||||
if val.startswith('"') and val.endswith('"'):
|
||||
val = json.loads(val)
|
||||
meta[key.strip()] = val
|
||||
return meta
|
||||
|
||||
|
||||
def parse_frontmatter_normen(md: str) -> list[dict]:
|
||||
return parse_frontmatter(md).get("normen") or []
|
||||
|
||||
|
||||
def build_index(
|
||||
ge_md: Path,
|
||||
lawgit_root: Path,
|
||||
) -> dict:
|
||||
by_norm: dict[str, list[dict]] = defaultdict(list)
|
||||
unmapped: list[dict] = []
|
||||
decisions = 0
|
||||
|
||||
for path in sorted(ge_md.rglob("*.md")):
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
meta = parse_frontmatter(text)
|
||||
if not meta.get("doknr"):
|
||||
continue
|
||||
decisions += 1
|
||||
entry_base = {
|
||||
"doknr": meta.get("doknr"),
|
||||
"gericht": meta.get("gericht"),
|
||||
"ebene": meta.get("ebene"),
|
||||
"aktenzeichen": meta.get("aktenzeichen"),
|
||||
"datum": meta.get("datum"),
|
||||
"path": str(path.relative_to(ge_md)),
|
||||
"doktyp": meta.get("doktyp"),
|
||||
"hat_leitsatz": bool(
|
||||
re.search(r"^## Leitsatz\s*$", text, re.M)
|
||||
and re.search(
|
||||
r"^## Leitsatz\s*\n\n.+\n", text, re.M | re.S
|
||||
)
|
||||
),
|
||||
}
|
||||
norms = meta.get("normen") or []
|
||||
if not norms:
|
||||
unmapped.append({**entry_base, "grund": "keine_normen"})
|
||||
continue
|
||||
for n in norms:
|
||||
slug = map_gesetz_slug(n.get("gesetz", ""))
|
||||
para = n.get("paragraph", "")
|
||||
key = f"{slug}/§{para}" if para else slug
|
||||
lg_path, ok = lawgit_path_for(lawgit_root, slug, para)
|
||||
item = {
|
||||
**entry_base,
|
||||
"roh": n.get("roh"),
|
||||
"gesetz": slug,
|
||||
"paragraph": para,
|
||||
"lawgit": str(lg_path.relative_to(lawgit_root)) if lg_path and ok else None,
|
||||
"mapped": ok,
|
||||
}
|
||||
if ok:
|
||||
by_norm[key].append(item)
|
||||
else:
|
||||
unmapped.append({**item, "grund": "kein_lawgit_pfad"})
|
||||
|
||||
return {
|
||||
"lawgit_root": str(lawgit_root.resolve()),
|
||||
"ge_md": str(ge_md.resolve()),
|
||||
"entscheidungen": decisions,
|
||||
"normen": {k: v for k, v in sorted(by_norm.items())},
|
||||
"unmapped": unmapped,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Normen-Index aus ge_md/ bauen")
|
||||
parser.add_argument(
|
||||
"--lawgit",
|
||||
default=os.environ.get("LAWGIT_ROOT", "../lawgit"),
|
||||
help="Pfad zu lawgit (Default: ../lawgit oder LAWGIT_ROOT)",
|
||||
)
|
||||
parser.add_argument("--ge-md", default="ge_md", help="Verzeichnis der Entscheidungen")
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
default="data/normen_index.json",
|
||||
help="Ausgabedatei",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
lawgit = Path(args.lawgit).resolve()
|
||||
ge_md = Path(args.ge_md).resolve()
|
||||
if not ge_md.is_dir():
|
||||
print(f"ge_md fehlt: {ge_md}", file=sys.stderr)
|
||||
return 1
|
||||
if not (lawgit / "laws_md").is_dir():
|
||||
print(
|
||||
f"Warnung: {lawgit}/laws_md fehlt — Mapping wird überwiegend unmapped.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
index = build_index(ge_md, lawgit)
|
||||
out = Path(args.out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(
|
||||
json.dumps(index, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
mapped_norms = len(index["normen"])
|
||||
print(
|
||||
f"Index: {index['entscheidungen']} Entscheidungen, "
|
||||
f"{mapped_norms} Norm-Schlüssel, "
|
||||
f"{len(index['unmapped'])} unmapped → {out}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
10
kommentar/README.md
Normal file
10
kommentar/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Kommentar
|
||||
|
||||
Hier entstehen später Kommentare im Stil von BeckOK / Grüneberg:
|
||||
eine Datei pro Paragraph, z. B. `bgb/§242.md`, mit Randnummern.
|
||||
|
||||
Der geltende Normtext bleibt in Lawgit (`laws_md/`). Neue
|
||||
Gerichtsentscheidungen aus `ge_md/` werden nach den Regeln in
|
||||
[docs/EINARBEITUNG.md](../docs/EINARBEITUNG.md) eingearbeitet.
|
||||
|
||||
Noch keine Kommentartexte — nur der Anker für den nächsten Schritt.
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
requests>=2.31.0
|
||||
457
rii_to_markdown.py
Normal file
457
rii_to_markdown.py
Normal file
@@ -0,0 +1,457 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RII-XML (rechtsprechung-im-internet.de, rii-dok.dtd) → Markdown mit YAML-Kopf.
|
||||
|
||||
Eine Entscheidung wird eine Datei unter ge_md/<gericht>/<jahr>/<az-slug>.md.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# Gerichtstyp → Ebene und Ordnername
|
||||
COURT_MAP: dict[str, tuple[str, str]] = {
|
||||
"BVerfG": ("verfg", "bverfg"),
|
||||
"BGH": ("bund", "bgh"),
|
||||
"BVerwG": ("bund", "bverwg"),
|
||||
"BFH": ("bund", "bfh"),
|
||||
"BAG": ("bund", "bag"),
|
||||
"BSG": ("bund", "bsg"),
|
||||
"BPatG": ("bund", "bpatg"),
|
||||
}
|
||||
|
||||
BUND_COURTS = {"BGH", "BVerwG", "BFH", "BAG", "BSG", "BPatG"}
|
||||
|
||||
|
||||
def local_tag(tag: str | None) -> str:
|
||||
if not tag:
|
||||
return ""
|
||||
if "}" in tag:
|
||||
return tag.rsplit("}", 1)[-1]
|
||||
return tag
|
||||
|
||||
|
||||
def elem_text(elem: ET.Element | None) -> str:
|
||||
if elem is None:
|
||||
return ""
|
||||
return "".join(elem.itertext()).strip()
|
||||
|
||||
|
||||
def first_child(parent: ET.Element, name: str) -> ET.Element | None:
|
||||
for child in parent:
|
||||
if local_tag(child.tag) == name:
|
||||
return child
|
||||
return None
|
||||
|
||||
|
||||
def all_children(parent: ET.Element, name: str) -> list[ET.Element]:
|
||||
return [c for c in parent if local_tag(c.tag) == name]
|
||||
|
||||
|
||||
def find_deep(parent: ET.Element, name: str) -> ET.Element | None:
|
||||
for elem in parent.iter():
|
||||
if local_tag(elem.tag) == name:
|
||||
return elem
|
||||
return None
|
||||
|
||||
|
||||
def xml_fragment_to_markdown(elem: ET.Element | None) -> str:
|
||||
"""HTML/XML-Fragment (P, BR, DL, …) zu lesbarem Markdown."""
|
||||
if elem is None:
|
||||
return ""
|
||||
|
||||
result: list[str] = []
|
||||
|
||||
def process_list(dl_elem: ET.Element) -> None:
|
||||
for child in dl_elem:
|
||||
tag = local_tag(child.tag)
|
||||
if tag == "DT":
|
||||
if result and result[-1] not in ("\n", "\n\n"):
|
||||
result.append("\n")
|
||||
process_element(child)
|
||||
elif tag == "DD":
|
||||
process_element(child)
|
||||
result.append("\n")
|
||||
else:
|
||||
process_element(child)
|
||||
|
||||
def process_element(e: ET.Element) -> None:
|
||||
if e.text and e.text.strip():
|
||||
result.append(e.text.strip())
|
||||
|
||||
for child in e:
|
||||
tag = local_tag(child.tag)
|
||||
if tag == "P":
|
||||
if result and result[-1] != "\n\n":
|
||||
result.append("\n\n")
|
||||
process_element(child)
|
||||
result.append("\n\n")
|
||||
elif tag == "BR":
|
||||
result.append("\n")
|
||||
if child.text and child.text.strip():
|
||||
result.append(child.text.strip())
|
||||
elif tag == "DL":
|
||||
process_list(child)
|
||||
elif tag in ("DT", "DD"):
|
||||
if tag == "DT" and result and result[-1] not in ("\n", "\n\n"):
|
||||
result.append("\n")
|
||||
process_element(child)
|
||||
if tag == "DD":
|
||||
result.append("\n")
|
||||
elif tag == "LA":
|
||||
process_element(child)
|
||||
elif tag in ("table", "tgroup", "tbody", "thead", "row", "entry"):
|
||||
if tag in ("table", "tgroup"):
|
||||
result.append("\n[Tabelle]\n")
|
||||
else:
|
||||
process_element(child)
|
||||
|
||||
if child.tail and child.tail.strip():
|
||||
result.append(child.tail.strip())
|
||||
|
||||
process_element(elem)
|
||||
text = "".join(result)
|
||||
text = (
|
||||
text.replace(""", '"')
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(" ", " ")
|
||||
.replace("§", "§")
|
||||
)
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def iso_date(raw: str) -> str:
|
||||
s = (raw or "").strip()
|
||||
m = re.match(r"^(\d{4})-(\d{2})-(\d{2})", s)
|
||||
if m:
|
||||
return f"{m.group(1)}-{m.group(2)}-{m.group(3)}"
|
||||
m = re.match(r"^(\d{2})\.(\d{2})\.(\d{4})", s)
|
||||
if m:
|
||||
return f"{m.group(3)}-{m.group(2)}-{m.group(1)}"
|
||||
m = re.match(r"^(\d{4})(\d{2})(\d{2})$", s)
|
||||
if m:
|
||||
return f"{m.group(1)}-{m.group(2)}-{m.group(3)}"
|
||||
return ""
|
||||
|
||||
|
||||
def ebene_for(gertyp: str) -> str:
|
||||
g = (gertyp or "").strip()
|
||||
if g in COURT_MAP:
|
||||
return COURT_MAP[g][0]
|
||||
if g.upper().startswith("OLG") or g.upper().startswith("LG"):
|
||||
return "olg"
|
||||
if g in BUND_COURTS:
|
||||
return "bund"
|
||||
return "bund"
|
||||
|
||||
|
||||
def court_dir(gertyp: str) -> str:
|
||||
g = (gertyp or "").strip()
|
||||
if g in COURT_MAP:
|
||||
return COURT_MAP[g][1]
|
||||
slug = re.sub(r"[^a-z0-9]+", "-", g.lower()).strip("-")
|
||||
return slug or "unbekannt"
|
||||
|
||||
|
||||
def az_slug(aktenzeichen: str, doknr: str = "") -> str:
|
||||
s = (
|
||||
aktenzeichen.lower()
|
||||
.replace("ä", "ae")
|
||||
.replace("ö", "oe")
|
||||
.replace("ü", "ue")
|
||||
.replace("ß", "ss")
|
||||
)
|
||||
s = re.sub(r"[\s/]+", "-", s)
|
||||
s = re.sub(r"[^a-z0-9-]", "", s)
|
||||
s = re.sub(r"-+", "-", s).strip("-")
|
||||
if len(s) <= 80:
|
||||
return s or (doknr.lower() if doknr else "ohne-az")
|
||||
suf = "-" + re.sub(r"[^a-z0-9]", "", doknr.lower()) if doknr else ""
|
||||
return s[: max(1, 80 - len(suf))].rstrip("-") + suf
|
||||
|
||||
|
||||
def parse_norm_string(roh: str) -> dict[str, str] | None:
|
||||
"""
|
||||
Einzelnen Norm-String zu {gesetz, paragraph, roh} zerlegen.
|
||||
|
||||
Beispiele:
|
||||
§ 242 BGB
|
||||
§§ 133, 134 InsO
|
||||
Art. 2 Abs. 1 GG
|
||||
§ 31 Abs. 1 BVerfGG
|
||||
"""
|
||||
roh = (roh or "").strip()
|
||||
if not roh:
|
||||
return None
|
||||
|
||||
# Gesetz am Ende: Abkürzung (Buchstaben/Ziffern, mind. 2 Zeichen)
|
||||
m = re.search(
|
||||
r"^(?:§{1,2}|Art\.?)\s*(.+?)\s+([A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß0-9.\-]{1,})\s*$",
|
||||
roh,
|
||||
)
|
||||
if not m:
|
||||
# nur Abkürzung ohne Paragraph
|
||||
m2 = re.match(r"^([A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß0-9.\-]{1,})$", roh)
|
||||
if m2:
|
||||
return {"gesetz": m2.group(1).lower(), "paragraph": "", "roh": roh}
|
||||
return {"gesetz": "", "paragraph": "", "roh": roh}
|
||||
|
||||
middle, gesetz = m.group(1).strip(), m.group(2).strip()
|
||||
# erste Paragraphen-/Artikelnummer
|
||||
num = re.search(r"(\d+[a-z]?)", middle, re.I)
|
||||
paragraph = num.group(1) if num else ""
|
||||
return {
|
||||
"gesetz": gesetz.lower().replace("ä", "ae").replace("ö", "oe").replace("ü", "ue"),
|
||||
"paragraph": paragraph,
|
||||
"roh": roh,
|
||||
}
|
||||
|
||||
|
||||
def collect_normen(root: ET.Element) -> list[dict[str, str]]:
|
||||
raw_parts: list[str] = []
|
||||
for norm_el in root.iter():
|
||||
if local_tag(norm_el.tag) != "norm":
|
||||
continue
|
||||
text = elem_text(norm_el)
|
||||
if not text:
|
||||
continue
|
||||
# Zeilen, Semikolon, und Komma vor neuem §/Art. trennen
|
||||
chunks = re.split(r"[\n;]+|,\s*(?=§|Art\.?\s)", text)
|
||||
for part in chunks:
|
||||
part = part.strip().rstrip(",").strip()
|
||||
if not part:
|
||||
continue
|
||||
if re.search(r"(§{1,2}|Art\.?)\s*\d", part, re.I):
|
||||
raw_parts.append(part)
|
||||
elif re.match(r"^[A-Za-zÄÖÜäöüß]", part):
|
||||
raw_parts.append(part)
|
||||
|
||||
seen: set[str] = set()
|
||||
out: list[dict[str, str]] = []
|
||||
for roh in raw_parts:
|
||||
if roh in seen:
|
||||
continue
|
||||
seen.add(roh)
|
||||
parsed = parse_norm_string(roh)
|
||||
if parsed:
|
||||
out.append(parsed)
|
||||
return out
|
||||
|
||||
|
||||
def collect_vorinstanz(root: ET.Element) -> list[str]:
|
||||
out: list[str] = []
|
||||
for el in root.iter():
|
||||
if local_tag(el.tag) != "vorinstanz":
|
||||
continue
|
||||
t = elem_text(el)
|
||||
if t:
|
||||
out.append(t)
|
||||
else:
|
||||
# strukturierte Vorinstanz: Gericht + AZ
|
||||
bits = []
|
||||
for child in el:
|
||||
ct = elem_text(child)
|
||||
if ct:
|
||||
bits.append(ct)
|
||||
if bits:
|
||||
out.append(" — ".join(bits))
|
||||
return out
|
||||
|
||||
|
||||
def yaml_scalar(value: str) -> str:
|
||||
"""YAML-sicherer Skalar (JSON-String-Stil)."""
|
||||
import json
|
||||
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
|
||||
|
||||
def build_frontmatter(meta: dict[str, Any]) -> str:
|
||||
lines = ["---"]
|
||||
for key in (
|
||||
"ebene",
|
||||
"gericht",
|
||||
"spruchkoerper",
|
||||
"datum",
|
||||
"aktenzeichen",
|
||||
"ecli",
|
||||
"doknr",
|
||||
"doktyp",
|
||||
"quelle",
|
||||
):
|
||||
val = meta.get(key, "")
|
||||
if val is None or val == "":
|
||||
continue
|
||||
lines.append(f"{key}: {yaml_scalar(str(val))}")
|
||||
|
||||
normen = meta.get("normen") or []
|
||||
if normen:
|
||||
lines.append("normen:")
|
||||
for n in normen:
|
||||
gesetz = yaml_scalar(n.get("gesetz", ""))
|
||||
paragraph = yaml_scalar(n.get("paragraph", ""))
|
||||
roh = yaml_scalar(n.get("roh", ""))
|
||||
lines.append(
|
||||
f" - {{ gesetz: {gesetz}, paragraph: {paragraph}, roh: {roh} }}"
|
||||
)
|
||||
else:
|
||||
lines.append("normen: []")
|
||||
|
||||
vor = meta.get("vorinstanz") or []
|
||||
if vor:
|
||||
lines.append("vorinstanz:")
|
||||
for v in vor:
|
||||
lines.append(f" - {yaml_scalar(v)}")
|
||||
else:
|
||||
lines.append("vorinstanz: []")
|
||||
|
||||
lines.append("---")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def section_markdown(root: ET.Element, tag_names: tuple[str, ...]) -> str:
|
||||
for name in tag_names:
|
||||
el = find_deep(root, name)
|
||||
if el is not None:
|
||||
# Wenn das Element Kinder mit Markup hat, fragmentarisch konvertieren
|
||||
if list(el):
|
||||
md = xml_fragment_to_markdown(el)
|
||||
else:
|
||||
md = elem_text(el)
|
||||
if md:
|
||||
return md
|
||||
return ""
|
||||
|
||||
|
||||
def parse_rii_xml(xml_content: str | bytes) -> dict[str, Any] | None:
|
||||
"""Parst ein RII-Dokument. Rückgabe: Meta + Body-Abschnitte oder None."""
|
||||
if isinstance(xml_content, bytes):
|
||||
xml_content = xml_content.decode("utf-8", errors="replace")
|
||||
|
||||
# Externe DTD nicht laden; DOCTYPE entfernen
|
||||
xml_content = re.sub(r"<!DOCTYPE[^>]*>", "", xml_content, count=1, flags=re.I)
|
||||
cleaned = re.sub(r"&(?!(amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);)", "&", xml_content)
|
||||
try:
|
||||
root = ET.fromstring(cleaned)
|
||||
except ET.ParseError:
|
||||
return None
|
||||
|
||||
doknr = elem_text(find_deep(root, "doknr")) or root.get("doknr", "")
|
||||
gertyp = elem_text(find_deep(root, "gertyp")) or "unbekannt"
|
||||
az = elem_text(find_deep(root, "aktenzeichen"))
|
||||
if not az and not doknr:
|
||||
return None
|
||||
|
||||
datum = iso_date(elem_text(find_deep(root, "entsch-datum")))
|
||||
ecli = elem_text(find_deep(root, "ecli"))
|
||||
doktyp = elem_text(find_deep(root, "doktyp")) or "Entscheidung"
|
||||
spruch = elem_text(find_deep(root, "spruchkoerper"))
|
||||
|
||||
meta = {
|
||||
"ebene": ebene_for(gertyp),
|
||||
"gericht": gertyp,
|
||||
"spruchkoerper": spruch,
|
||||
"datum": datum,
|
||||
"aktenzeichen": az,
|
||||
"ecli": ecli,
|
||||
"doknr": doknr,
|
||||
"doktyp": doktyp,
|
||||
"quelle": "rii",
|
||||
"normen": collect_normen(root),
|
||||
"vorinstanz": collect_vorinstanz(root),
|
||||
}
|
||||
|
||||
body = {
|
||||
"leitsatz": section_markdown(root, ("leitsatz",)),
|
||||
"tenor": section_markdown(root, ("tenor",)),
|
||||
"tatbestand": section_markdown(root, ("tatbestand",)),
|
||||
"gruende": section_markdown(
|
||||
root, ("entscheidungsgruende", "gruende")
|
||||
),
|
||||
"abwmeinung": section_markdown(root, ("abwmeinung",)),
|
||||
"titelzeile": section_markdown(root, ("titelzeile",)),
|
||||
}
|
||||
return {"meta": meta, "body": body}
|
||||
|
||||
|
||||
def to_markdown(parsed: dict[str, Any]) -> str:
|
||||
meta = parsed["meta"]
|
||||
body = parsed["body"]
|
||||
parts = [build_frontmatter(meta), ""]
|
||||
|
||||
title_bits = [meta.get("gericht") or "", meta.get("doktyp") or ""]
|
||||
if meta.get("aktenzeichen"):
|
||||
title_bits.append(f"— {meta['aktenzeichen']}")
|
||||
if meta.get("datum"):
|
||||
ddmmyyyy = "-".join(reversed(meta["datum"].split("-"))) if meta["datum"] else ""
|
||||
# datum is YYYY-MM-DD → DD.MM.YYYY
|
||||
if meta["datum"] and len(meta["datum"]) == 10:
|
||||
y, m, d = meta["datum"].split("-")
|
||||
ddmmyyyy = f"{d}.{m}.{y}"
|
||||
title_bits.append(f"vom {ddmmyyyy}")
|
||||
parts.append("# " + " ".join(b for b in title_bits if b).strip())
|
||||
parts.append("")
|
||||
|
||||
if body.get("titelzeile"):
|
||||
parts.extend(["## Titelzeile", "", body["titelzeile"], ""])
|
||||
if body.get("leitsatz"):
|
||||
parts.extend(["## Leitsatz", "", body["leitsatz"], ""])
|
||||
if body.get("tenor"):
|
||||
parts.extend(["## Tenor", "", body["tenor"], ""])
|
||||
if body.get("tatbestand"):
|
||||
parts.extend(["## Tatbestand", "", body["tatbestand"], ""])
|
||||
if body.get("gruende"):
|
||||
parts.extend(["## Gründe", "", body["gruende"], ""])
|
||||
if body.get("abwmeinung"):
|
||||
parts.extend(["## Abweichende Meinung", "", body["abwmeinung"], ""])
|
||||
|
||||
text = "\n".join(parts).rstrip() + "\n"
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
return text
|
||||
|
||||
|
||||
def relative_path(meta: dict[str, Any]) -> Path:
|
||||
"""Relativer Pfad unter ge_md/: <court>/<year>/<az-slug>.md"""
|
||||
court = court_dir(meta.get("gericht", ""))
|
||||
year = (meta.get("datum") or "")[:4] or "ohne-jahr"
|
||||
slug = az_slug(meta.get("aktenzeichen", ""), meta.get("doknr", ""))
|
||||
return Path(court) / year / f"{slug}.md"
|
||||
|
||||
|
||||
def convert_xml_to_markdown_file(
|
||||
xml_content: str | bytes,
|
||||
output_base: Path | str = "ge_md",
|
||||
) -> Path | None:
|
||||
"""Schreibt Markdown-Datei; Rückgabe: Pfad oder None."""
|
||||
parsed = parse_rii_xml(xml_content)
|
||||
if not parsed:
|
||||
return None
|
||||
rel = relative_path(parsed["meta"])
|
||||
out = Path(output_base) / rel
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(to_markdown(parsed), encoding="utf-8")
|
||||
return out
|
||||
|
||||
|
||||
def doknr_already_exists(doknr: str, output_base: Path | str = "ge_md") -> Path | None:
|
||||
"""Sucht eine vorhandene Markdown-Datei mit dieser doknr im YAML-Kopf."""
|
||||
if not doknr:
|
||||
return None
|
||||
base = Path(output_base)
|
||||
if not base.is_dir():
|
||||
return None
|
||||
needle = f'doknr: "{doknr}"'
|
||||
needle2 = f"doknr: {doknr}"
|
||||
for path in base.rglob("*.md"):
|
||||
try:
|
||||
head = path.read_text(encoding="utf-8", errors="replace")[:800]
|
||||
except OSError:
|
||||
continue
|
||||
if needle in head or f"doknr: {yaml_scalar(doknr)}" in head or needle2 in head:
|
||||
return path
|
||||
return None
|
||||
150
setup-ssh-deploy.sh
Executable file
150
setup-ssh-deploy.sh
Executable file
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
# SSH Deploy Key Setup for explore
|
||||
#
|
||||
# Automatisiert:
|
||||
# 1. Erzeugt ein SSH-Schlüsselpaar (nur für Deployment)
|
||||
# 2. Hinterlegt den Public Key auf dem Server
|
||||
# 3. Speichert den Private Key als Gitea Secret via API
|
||||
#
|
||||
# Voraussetzungen:
|
||||
# - ssh-keygen, jq, curl
|
||||
# - SSH-Zugang zum Server (z.B. ~/.ssh/gitea_deploy)
|
||||
# - Gitea API-Token (mit repo-Rechten)
|
||||
# - Repo michaelkole/explore muss bereits existieren
|
||||
#
|
||||
# Usage: ./setup-ssh-deploy.sh
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_NAME="explore"
|
||||
SERVER_IP="82.165.92.92"
|
||||
SERVER_USER="root"
|
||||
GITEA_HOST="git.coded.law"
|
||||
GITEA_OWNER="michaelkole"
|
||||
GITEA_REPO="explore"
|
||||
KEY_FILE="$HOME/.ssh/deploy_${PROJECT_NAME}"
|
||||
SECRET_NAME="SSH_PRIVATE_KEY"
|
||||
|
||||
echo ""
|
||||
echo "SSH Deploy Key Setup für $PROJECT_NAME"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
echo "Gitea API-Token wird benötigt (mit repo-Rechten)."
|
||||
echo "Erstellen unter: https://$GITEA_HOST/user/settings/applications"
|
||||
echo ""
|
||||
read -rsp "Gitea API-Token: " GITEA_TOKEN
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "Fehler: Kein Token angegeben."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Prüfe Gitea-Zugang..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://$GITEA_HOST/api/v1/repos/$GITEA_OWNER/$GITEA_REPO")
|
||||
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo "Fehler: Gitea API antwortet mit HTTP $HTTP_CODE."
|
||||
echo "Prüfe Token und Repo-Pfad: $GITEA_OWNER/$GITEA_REPO"
|
||||
echo "Repo ggf. zuerst anlegen unter https://$GITEA_HOST/repo/create"
|
||||
exit 1
|
||||
fi
|
||||
echo " Gitea OK"
|
||||
|
||||
if [ -f "$KEY_FILE" ]; then
|
||||
echo ""
|
||||
echo "SSH Key existiert bereits: $KEY_FILE"
|
||||
read -rp "Überschreiben? (j/N) " OVERWRITE
|
||||
if [[ ! "$OVERWRITE" =~ ^[jJyY]$ ]]; then
|
||||
echo "Verwende bestehenden Key."
|
||||
else
|
||||
rm -f "$KEY_FILE" "$KEY_FILE.pub"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "$KEY_FILE" ]; then
|
||||
echo "Erzeuge SSH-Schlüsselpaar..."
|
||||
ssh-keygen -t ed25519 -C "deploy@$PROJECT_NAME" -f "$KEY_FILE" -N ""
|
||||
chmod 600 "$KEY_FILE"
|
||||
echo " Key erzeugt: $KEY_FILE"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Hinterlege Public Key auf $SERVER_USER@$SERVER_IP..."
|
||||
|
||||
SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=5"
|
||||
EXISTING_KEY=""
|
||||
|
||||
if ! ssh $SSH_OPTS -o BatchMode=yes "$SERVER_USER@$SERVER_IP" "true" 2>/dev/null; then
|
||||
echo " Direkter SSH-Zugang nicht möglich (nur Key-Auth?)."
|
||||
echo " Suche bestehenden SSH-Key..."
|
||||
for candidate in ~/.ssh/id_ed25519 ~/.ssh/id_rsa ~/.ssh/gitea_deploy; do
|
||||
if [ -f "$candidate" ] && ssh -i "$candidate" $SSH_OPTS -o BatchMode=yes "$SERVER_USER@$SERVER_IP" "true" 2>/dev/null; then
|
||||
EXISTING_KEY="$candidate"
|
||||
echo " Gefunden: $candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$EXISTING_KEY" ]; then
|
||||
echo ""
|
||||
echo " Kein funktionierender SSH-Key gefunden."
|
||||
echo " Bitte manuell:"
|
||||
echo " cat $KEY_FILE.pub | ssh -i ~/.ssh/gitea_deploy $SERVER_USER@$SERVER_IP 'cat >> ~/.ssh/authorized_keys'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$EXISTING_KEY" ]; then
|
||||
ssh -i "$EXISTING_KEY" "$SERVER_USER@$SERVER_IP" "mkdir -p ~/.ssh && chmod 700 ~/.ssh"
|
||||
cat "$KEY_FILE.pub" | ssh -i "$EXISTING_KEY" "$SERVER_USER@$SERVER_IP" "cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
|
||||
else
|
||||
ssh "$SERVER_USER@$SERVER_IP" "mkdir -p ~/.ssh && chmod 700 ~/.ssh"
|
||||
cat "$KEY_FILE.pub" | ssh "$SERVER_USER@$SERVER_IP" "cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
|
||||
fi
|
||||
echo " Public Key auf Server hinterlegt"
|
||||
|
||||
echo "Teste SSH-Verbindung mit Deploy Key..."
|
||||
if ssh -i "$KEY_FILE" $SSH_OPTS -o BatchMode=yes "$SERVER_USER@$SERVER_IP" "echo 'SSH OK'" 2>/dev/null; then
|
||||
echo " SSH-Verbindung OK"
|
||||
else
|
||||
echo " WARNUNG: SSH-Test fehlgeschlagen. Prüfe authorized_keys auf dem Server."
|
||||
fi
|
||||
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "Fehler: jq fehlt (brew install jq)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Speichere Private Key als Gitea Secret '$SECRET_NAME'..."
|
||||
PRIVATE_KEY_CONTENT=$(cat "$KEY_FILE")
|
||||
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-X PUT \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"data\": $(echo "$PRIVATE_KEY_CONTENT" | jq -Rs .)}" \
|
||||
"https://$GITEA_HOST/api/v1/repos/$GITEA_OWNER/$GITEA_REPO/actions/secrets/$SECRET_NAME")
|
||||
|
||||
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "204" ]; then
|
||||
echo " Secret '$SECRET_NAME' gespeichert"
|
||||
else
|
||||
echo " Fehler: Gitea API antwortet mit HTTP $HTTP_CODE"
|
||||
echo " Manuell: https://$GITEA_HOST/$GITEA_OWNER/$GITEA_REPO/settings/actions/secrets"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "SSH Deploy Key Setup abgeschlossen!"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo " Private Key: $KEY_FILE"
|
||||
echo " Public Key: $KEY_FILE.pub"
|
||||
echo " Server: $SERVER_USER@$SERVER_IP"
|
||||
echo " Gitea Secret: $SECRET_NAME ($GITEA_OWNER/$GITEA_REPO)"
|
||||
echo ""
|
||||
66
sync_to_server.sh
Executable file
66
sync_to_server.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
# Spiegelt Explor.Code.Law auf den Strato-VServer (rsync).
|
||||
# Nützlich bevor das Gitea-Repo existiert, und für ge_xml/ (gitignoriert).
|
||||
#
|
||||
# Usage:
|
||||
# ./sync_to_server.sh # Code + ge_md + data + ge_xml
|
||||
# ./sync_to_server.sh --xml-only
|
||||
# ./sync_to_server.sh --dry-run
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
SERVER="root@82.165.92.92"
|
||||
SSH_KEY="${SSH_KEY:-$HOME/.ssh/gitea_deploy}"
|
||||
REMOTE_DIR="/opt/explore"
|
||||
RSYNC_SSH="ssh -i $SSH_KEY -o BatchMode=yes -o StrictHostKeyChecking=accept-new"
|
||||
|
||||
XML_ONLY=0
|
||||
DRY=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--xml-only) XML_ONLY=1 ;;
|
||||
--dry-run) DRY=(--dry-run) ;;
|
||||
-h|--help)
|
||||
sed -n '2,12p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unbekanntes Argument: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ! -f "$SSH_KEY" ]; then
|
||||
echo "SSH-Key fehlt: $SSH_KEY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ssh -i "$SSH_KEY" -o BatchMode=yes "$SERVER" "mkdir -p $REMOTE_DIR/ge_md $REMOTE_DIR/data $REMOTE_DIR/ge_xml $REMOTE_DIR/kommentar"
|
||||
|
||||
if [ "$XML_ONLY" -eq 1 ]; then
|
||||
echo "Sync ge_xml/ → $SERVER:$REMOTE_DIR/ge_xml/"
|
||||
rsync -avz "${DRY[@]}" \
|
||||
-e "$RSYNC_SSH" \
|
||||
--exclude '.DS_Store' \
|
||||
"$ROOT/ge_xml/" "$SERVER:$REMOTE_DIR/ge_xml/"
|
||||
else
|
||||
echo "Sync Projekt → $SERVER:$REMOTE_DIR/"
|
||||
rsync -avz "${DRY[@]}" \
|
||||
-e "$RSYNC_SSH" \
|
||||
--exclude '.git/' \
|
||||
--exclude '.venv/' \
|
||||
--exclude 'venv/' \
|
||||
--exclude '__pycache__/' \
|
||||
--exclude '.pytest_cache/' \
|
||||
--exclude '.DS_Store' \
|
||||
--exclude '.env' \
|
||||
"$ROOT/" "$SERVER:$REMOTE_DIR/"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
ssh -i "$SSH_KEY" -o BatchMode=yes "$SERVER" \
|
||||
"echo ge_md: \$(find $REMOTE_DIR/ge_md -name '*.md' 2>/dev/null | wc -l); \
|
||||
echo ge_xml: \$(find $REMOTE_DIR/ge_xml -type f 2>/dev/null | wc -l); \
|
||||
echo index: \$([ -f $REMOTE_DIR/data/normen_index.json ] && wc -c < $REMOTE_DIR/data/normen_index.json || echo fehlt) bytes"
|
||||
echo "Fertig."
|
||||
32
tests/fixtures/bgh_ix_zr_72_20.xml
vendored
Normal file
32
tests/fixtures/bgh_ix_zr_72_20.xml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE dokument SYSTEM "https://www.rechtsprechung-im-internet.de/dtd/v1/rii-dok.dtd">
|
||||
<dokument>
|
||||
<doknr>KORE000000001</doknr>
|
||||
<ecli>ECLI:DE:BGH:2021:060521UIXZR72.20.0</ecli>
|
||||
<gertyp>BGH</gertyp>
|
||||
<gerort>Karlsruhe</gerort>
|
||||
<spruchkoerper>IX. Zivilsenat</spruchkoerper>
|
||||
<entsch-datum>2021-05-06</entsch-datum>
|
||||
<aktenzeichen>IX ZR 72/20</aktenzeichen>
|
||||
<doktyp>Urteil</doktyp>
|
||||
<norm>§ 133 InsO
|
||||
§ 242 BGB</norm>
|
||||
<titelzeile><P>Vorsatzanfechtung</P></titelzeile>
|
||||
<leitsatz>
|
||||
<P>Die erkannte Zahlungsunfähigkeit allein trägt den Vorsatz nicht.</P>
|
||||
</leitsatz>
|
||||
<tenor>
|
||||
<P>Auf die Revision wird das Urteil aufgehoben.</P>
|
||||
</tenor>
|
||||
<tatbestand>
|
||||
<P>Die Klägerin verlangt Rückgewähr.</P>
|
||||
</tatbestand>
|
||||
<entscheidungsgruende>
|
||||
<P>Die Revision ist begründet.</P>
|
||||
<P>I. Das Berufungsgericht hat angenommen, der Anfechtungsgegner habe vorsätzlich gehandelt.</P>
|
||||
</entscheidungsgruende>
|
||||
<vorinstanz>
|
||||
<gericht>OLG Hamm</gericht>
|
||||
<aktenzeichen>I-27 U 12/19</aktenzeichen>
|
||||
</vorinstanz>
|
||||
</dokument>
|
||||
25
tests/fixtures/bverfg_1_bvr_1585_19.xml
vendored
Normal file
25
tests/fixtures/bverfg_1_bvr_1585_19.xml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE dokument SYSTEM "https://www.rechtsprechung-im-internet.de/dtd/v1/rii-dok.dtd">
|
||||
<dokument>
|
||||
<doknr>KVRE000000002</doknr>
|
||||
<ecli>ECLI:DE:BVerfG:2020:rs20200505.1bvr158519</ecli>
|
||||
<gertyp>BVerfG</gertyp>
|
||||
<gerort>Karlsruhe</gerort>
|
||||
<spruchkoerper>1. Senat</spruchkoerper>
|
||||
<entsch-datum>20200505</entsch-datum>
|
||||
<aktenzeichen>1 BvR 1585/19</aktenzeichen>
|
||||
<doktyp>Beschluss</doktyp>
|
||||
<norm>Art. 2 Abs. 1 GG
|
||||
Art. 1 Abs. 1 GG
|
||||
§ 31 BVerfGG</norm>
|
||||
<titelzeile><P>Recht auf informationelle Selbstbestimmung</P></titelzeile>
|
||||
<leitsatz>
|
||||
<P>Das allgemeine Persönlichkeitsrecht schützt auch vor der unkontrollierten Weitergabe von Daten.</P>
|
||||
</leitsatz>
|
||||
<tenor>
|
||||
<P>Die Verfassungsbeschwerde wird nicht zur Entscheidung angenommen.</P>
|
||||
</tenor>
|
||||
<gruende>
|
||||
<P>Die Annahmevoraussetzungen des § 93a Abs. 2 BVerfGG liegen nicht vor.</P>
|
||||
</gruende>
|
||||
</dokument>
|
||||
192
tests/test_rii.py
Normal file
192
tests/test_rii.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline-Tests für rii_to_markdown und index_normen."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from rii_to_markdown import ( # noqa: E402
|
||||
az_slug,
|
||||
convert_xml_to_markdown_file,
|
||||
ebene_for,
|
||||
parse_norm_string,
|
||||
parse_rii_xml,
|
||||
relative_path,
|
||||
to_markdown,
|
||||
)
|
||||
from index_normen import ( # noqa: E402
|
||||
lawgit_path_for,
|
||||
map_gesetz_slug,
|
||||
parse_frontmatter_normen,
|
||||
)
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures"
|
||||
|
||||
|
||||
class TestEbene(unittest.TestCase):
|
||||
def test_bverfg(self):
|
||||
self.assertEqual(ebene_for("BVerfG"), "verfg")
|
||||
|
||||
def test_bgh(self):
|
||||
self.assertEqual(ebene_for("BGH"), "bund")
|
||||
|
||||
def test_olg(self):
|
||||
self.assertEqual(ebene_for("OLG München"), "olg")
|
||||
|
||||
|
||||
class TestParseNorm(unittest.TestCase):
|
||||
def test_paragraph_bgb(self):
|
||||
n = parse_norm_string("§ 242 BGB")
|
||||
self.assertEqual(n["gesetz"], "bgb")
|
||||
self.assertEqual(n["paragraph"], "242")
|
||||
self.assertEqual(n["roh"], "§ 242 BGB")
|
||||
|
||||
def test_artikel_gg(self):
|
||||
n = parse_norm_string("Art. 2 Abs. 1 GG")
|
||||
self.assertEqual(n["gesetz"], "gg")
|
||||
self.assertEqual(n["paragraph"], "2")
|
||||
|
||||
def test_comma_separated_norms(self):
|
||||
from rii_to_markdown import collect_normen
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
root = ET.fromstring(
|
||||
"<dokument><norm>§ 20 StGB, § 224 StGB, Art. 2 Abs. 1 GG</norm></dokument>"
|
||||
)
|
||||
norms = collect_normen(root)
|
||||
roh = {n["roh"] for n in norms}
|
||||
self.assertIn("§ 20 StGB", roh)
|
||||
self.assertIn("§ 224 StGB", roh)
|
||||
self.assertIn("Art. 2 Abs. 1 GG", roh)
|
||||
gg = next(n for n in norms if n["gesetz"] == "gg")
|
||||
self.assertEqual(gg["paragraph"], "2")
|
||||
|
||||
|
||||
class TestBghFixture(unittest.TestCase):
|
||||
def setUp(self):
|
||||
xml = (FIXTURES / "bgh_ix_zr_72_20.xml").read_text(encoding="utf-8")
|
||||
self.parsed = parse_rii_xml(xml)
|
||||
|
||||
def test_parses(self):
|
||||
self.assertIsNotNone(self.parsed)
|
||||
|
||||
def test_meta(self):
|
||||
m = self.parsed["meta"]
|
||||
self.assertEqual(m["ebene"], "bund")
|
||||
self.assertEqual(m["gericht"], "BGH")
|
||||
self.assertEqual(m["aktenzeichen"], "IX ZR 72/20")
|
||||
self.assertEqual(m["datum"], "2021-05-06")
|
||||
self.assertEqual(m["doknr"], "KORE000000001")
|
||||
self.assertEqual(m["doktyp"], "Urteil")
|
||||
self.assertEqual(m["quelle"], "rii")
|
||||
|
||||
def test_normen(self):
|
||||
roh = {n["roh"] for n in self.parsed["meta"]["normen"]}
|
||||
self.assertIn("§ 133 InsO", roh)
|
||||
self.assertIn("§ 242 BGB", roh)
|
||||
bgb = next(n for n in self.parsed["meta"]["normen"] if n["gesetz"] == "bgb")
|
||||
self.assertEqual(bgb["paragraph"], "242")
|
||||
|
||||
def test_body(self):
|
||||
b = self.parsed["body"]
|
||||
self.assertIn("Zahlungsunfähigkeit", b["leitsatz"])
|
||||
self.assertIn("aufgehoben", b["tenor"])
|
||||
self.assertIn("Rückgewähr", b["tatbestand"])
|
||||
self.assertIn("begründet", b["gruende"])
|
||||
|
||||
def test_vorinstanz(self):
|
||||
self.assertTrue(any("OLG Hamm" in v for v in self.parsed["meta"]["vorinstanz"]))
|
||||
|
||||
def test_markdown_sections(self):
|
||||
md = to_markdown(self.parsed)
|
||||
self.assertIn("ebene: \"bund\"", md)
|
||||
self.assertIn("## Leitsatz", md)
|
||||
self.assertIn("## Tenor", md)
|
||||
self.assertIn("## Tatbestand", md)
|
||||
self.assertIn("## Gründe", md)
|
||||
self.assertIn("gesetz: \"bgb\"", md)
|
||||
|
||||
def test_path(self):
|
||||
rel = relative_path(self.parsed["meta"])
|
||||
self.assertEqual(rel, Path("bgh/2021/ix-zr-72-20.md"))
|
||||
|
||||
def test_write_file(self):
|
||||
xml = (FIXTURES / "bgh_ix_zr_72_20.xml").read_bytes()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out = convert_xml_to_markdown_file(xml, tmp)
|
||||
self.assertIsNotNone(out)
|
||||
self.assertTrue(out.exists())
|
||||
self.assertEqual(out.name, "ix-zr-72-20.md")
|
||||
|
||||
|
||||
class TestBverfgFixture(unittest.TestCase):
|
||||
def setUp(self):
|
||||
xml = (FIXTURES / "bverfg_1_bvr_1585_19.xml").read_text(encoding="utf-8")
|
||||
self.parsed = parse_rii_xml(xml)
|
||||
|
||||
def test_ebene_verfg(self):
|
||||
self.assertEqual(self.parsed["meta"]["ebene"], "verfg")
|
||||
self.assertEqual(self.parsed["meta"]["gericht"], "BVerfG")
|
||||
|
||||
def test_datum_yyyymmdd(self):
|
||||
self.assertEqual(self.parsed["meta"]["datum"], "2020-05-05")
|
||||
|
||||
def test_gruende_from_gruende_tag(self):
|
||||
self.assertIn("Annahmevoraussetzungen", self.parsed["body"]["gruende"])
|
||||
|
||||
def test_path(self):
|
||||
rel = relative_path(self.parsed["meta"])
|
||||
self.assertEqual(rel, Path("bverfg/2020/1-bvr-1585-19.md"))
|
||||
|
||||
def test_norm_gg(self):
|
||||
gesetze = {n["gesetz"] for n in self.parsed["meta"]["normen"]}
|
||||
self.assertIn("gg", gesetze)
|
||||
|
||||
|
||||
class TestAzSlug(unittest.TestCase):
|
||||
def test_simple(self):
|
||||
self.assertEqual(az_slug("VIII ZR 1/24"), "viii-zr-1-24")
|
||||
|
||||
def test_long_with_doknr(self):
|
||||
long_az = "A " + "B " * 60 + "1/24"
|
||||
slug = az_slug(long_az, "KORE123")
|
||||
self.assertLessEqual(len(slug), 80)
|
||||
self.assertTrue(slug.endswith("kore123") or "kore123" in slug)
|
||||
|
||||
|
||||
class TestIndexNormen(unittest.TestCase):
|
||||
def test_map_slug(self):
|
||||
self.assertEqual(map_gesetz_slug("BGB"), "bgb")
|
||||
self.assertEqual(map_gesetz_slug("GG"), "gg")
|
||||
self.assertEqual(map_gesetz_slug("InsO"), "inso")
|
||||
|
||||
def test_lawgit_path(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
(root / "laws_md" / "bgb").mkdir(parents=True)
|
||||
(root / "laws_md" / "bgb" / "§242.md").write_text("# § 242\n", encoding="utf-8")
|
||||
path, ok = lawgit_path_for(root, "bgb", "242")
|
||||
self.assertTrue(ok)
|
||||
self.assertTrue(str(path).endswith("§242.md"))
|
||||
|
||||
def test_frontmatter_normen(self):
|
||||
md = '''---
|
||||
doknr: "X"
|
||||
normen:
|
||||
- { gesetz: "bgb", paragraph: "242", roh: "§ 242 BGB" }
|
||||
---
|
||||
# Test
|
||||
'''
|
||||
norms = parse_frontmatter_normen(md)
|
||||
self.assertEqual(len(norms), 1)
|
||||
self.assertEqual(norms[0]["gesetz"], "bgb")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user