Compare commits

40 Commits

Author SHA1 Message Date
66c1c5c55f chore(changelog): update changelog for v0.4.0
All checks were successful
Build and Publish nightly package / build-and-publish (release) Successful in 33s
2025-11-27 18:58:29 +00:00
89c4a4a073 chore(version): bump version to 0.4.0
All checks were successful
Auto Changelog & (Release) / release (push) Successful in 8s
Build and Publish nightly package / build-and-publish (push) Successful in 33s
2025-11-27 19:58:18 +01:00
72cb0cc20f chore(changelog): update unreleased changelog 2025-11-27 18:57:52 +00:00
39f01b266f fix(archive): skip categories with zero or unset days config
All checks were successful
Auto Changelog & (Release) / release (push) Successful in 6s
Build and Publish nightly package / build-and-publish (push) Successful in 31s
2025-11-27 19:57:43 +01:00
31c3f7d438 feat(archive): enable multithreaded zstd compression 2025-11-27 19:57:43 +01:00
4ea3b28081 chore(changelog): update changelog for v0.3.0
All checks were successful
Build and Publish nightly package / build-and-publish (release) Successful in 33s
2025-11-27 17:23:12 +00:00
702aa2a75b chore(version): bump version to 0.3.0
Some checks failed
Auto Changelog & (Release) / release (push) Successful in 7s
Build and Publish nightly package / build-and-publish (push) Has been cancelled
2025-11-27 18:23:02 +01:00
529cd19ec6 chore(changelog): update unreleased changelog 2025-11-27 17:22:45 +00:00
fdff738fa7 chore(changelog): simplify release workflow using composite action
Some checks failed
Build and Publish nightly package / build-and-publish (push) Has been cancelled
Auto Changelog & (Release) / release (push) Successful in 5s
2025-11-27 18:22:34 +01:00
7f629fd752 feat(clean): add snapshot support for volatile repo cleanup
Some checks failed
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Failing after 6s
Build and Publish nightly package / build-and-publish (push) Successful in 33s
2025-11-27 18:21:20 +01:00
801c8e24a1 docs: fix typo in German documentation comment
Some checks failed
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Failing after 7s
Build and Publish nightly package / build-and-publish (push) Successful in 37s
2025-11-27 18:14:11 +01:00
f63ef85dd7 chore(changelog): update unreleased changelog 2025-05-11 13:35:57 +00:00
c2013fc205 chore(scripts): update file mode to make script executable
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
Build and Publish nightly package / build-and-publish (push) Successful in 19s
- Changes file mode from 644 to 755 to allow script execution
- Ensures proper permissions for usage in relevant environments
2025-05-11 15:35:41 +02:00
e5015ec2f3 chore(changelog): update unreleased changelog 2025-05-11 13:35:06 +00:00
1163744af9 chore(scripts): rename cleanup script for broader usage
Some checks failed
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
Build and Publish nightly package / build-and-publish (push) Failing after 17s
- Renames the cleanup_dev_versions script to cleanup_versions
2025-05-11 15:34:51 +02:00
e3c703aa32 chore(changelog): update unreleased changelog 2025-05-11 13:33:50 +00:00
aff319cc5f feat(workflows): add cleanup step for old dev versions
Some checks failed
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
Build and Publish nightly package / build-and-publish (push) Failing after 16s
- Introduces a step to remove old development versions using a script
- Helps maintain a cleaner environment during nightly builds
2025-05-11 15:33:35 +02:00
fc8b885214 feat(scripts): add script to clean old PyPI dev versions
- Introduces a Bash script to delete outdated PyPI dev versions
  from the Gitea package registry while keeping the latest version
- Uses environment variables for authentication and repository details
2025-05-11 15:33:35 +02:00
7067f0872b chore(gitignore): update ignored files for environment and build
- Add `.env` files to the ignore list to exclude environment configs
- Retain `dist/` directory in the ignore list for build artifacts
2025-05-11 15:33:35 +02:00
d5c2873d39 chore(changelog): update changelog for v0.2.0
All checks were successful
Build and Publish nightly package / build-and-publish (release) Successful in 18s
2025-05-11 12:57:51 +00:00
c005d1d38c chore(version): bump version to 0.2.0
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / changelog-only (push) Has been skipped
Build and Publish nightly package / build-and-publish (push) Successful in 17s
Auto Changelog & Release / release (push) Successful in 9s
2025-05-11 14:57:32 +02:00
5c0db1c656 chore(version): bump to 0.2.0
- Updates the project version to 0.2.0 in preparation for a new release
2025-05-11 14:57:24 +02:00
15671ac85b chore(deps): update dependencies for charset-normalizer and cryptography
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 11s
Build and Publish nightly package / build-and-publish (push) Successful in 21s
- Bump charset-normalizer from 3.4.1 to 3.4.2
- Bump cryptography from 44.0.2 to 44.0.3
- Adjust Click dependency constraint to "<8.2.0"

These updates enhance compatibility and address potential issues with
dependency versions.
2025-05-11 14:54:43 +02:00
18889a3352 chore(changelog): update unreleased changelog 2025-05-11 12:51:01 +00:00
ef6ac68f4b feat(deps): add click dependency and update poetry lockfile
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
Build and Publish nightly package / build-and-publish (push) Successful in 21s
2025-05-11 14:50:47 +02:00
307d18fa4d chore(changelog): update unreleased changelog 2025-05-11 12:41:07 +00:00
719f257739 feat(cli): add verbose option to status command
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 7s
Build and Publish nightly package / build-and-publish (push) Successful in 50s
- Introduces a `--verbose` flag to the `status` command for detailed output
- Displays repository details, including age, size, and Git status
- Enhances usability by providing more granular information on request
2025-05-11 14:40:50 +02:00
55e79d224f refactor(cli): streamline repository status logic
- Replace `RepoCategory` with `RepoCatalogState` for improved abstraction
- Simplify repository data processing and remove redundant functions
- Enhance code readability and maintainability by consolidating logic
2025-05-11 14:38:54 +02:00
68b3233fcb refactor(review): streamline repo review logic and data handling
- Replaces manual path handling with `RepoCatalogState` abstraction
- Simplifies repo status checks and removes redundant imports
- Improves clarity and maintainability of interactive review process
2025-05-11 14:38:44 +02:00
9bff626f30 refactor(cli): use RepoCatalogState for repository operations
- Replaces direct config usage with RepoCatalogState to manage categories
  and repositories, improving modularity and encapsulation.
- Updates repository movement logic to use `RepoCatalogState` methods
  for better abstraction and error handling.
2025-05-11 14:38:18 +02:00
458d965062 refactor(cli): simplify repository cleaning logic
- Replaced direct filesystem and Git operations with catalog-based API
- Streamlined handling of repository properties and deletion logic
- Improved readability and maintainability by reducing redundancy
2025-05-11 14:38:12 +02:00
d3ee7ee63d refactor(archive): streamline repository archiving logic
- Simplify archive directory resolution using catalog state
- Replace redundant directory checks with repository model methods
- Remove unused imports and legacy functions for cleaner code
- Improve error handling for missing archive categories
2025-05-11 14:38:06 +02:00
eb5bf52f2f feat(models): add catalog and filesystem utilities
- Introduce models for managing repository catalog and categories
- Add utilities for directory size and Git status checks
- Enable repository metadata management, validation, and operations
2025-05-11 14:37:53 +02:00
51e5c10a5d chore(changelog): update unreleased changelog 2025-05-11 12:21:19 +00:00
c596f46389 fix(scripts): update nightly version suffix format
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 7s
Build and Publish nightly package / build-and-publish (push) Successful in 16s
- Changes nightly version suffix to use `.dev` format for consistency
- Replaces `+nightly` with `.dev` followed by timestamp
2025-05-11 14:21:04 +02:00
71f397dd73 chore(changelog): update changelog for v0.1.0
All checks were successful
Build and Publish nightly package / build-and-publish (release) Successful in 18s
2025-05-11 12:18:03 +00:00
c4d1f3191a chore(version): add initial version file
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / changelog-only (push) Has been skipped
Build and Publish nightly package / build-and-publish (push) Successful in 20s
Auto Changelog & Release / release (push) Successful in 2m8s
- Introduces a VERSION file with the initial version `0.1.0`
2025-05-11 14:15:48 +02:00
66a5ddd1b0 chore(gitignore): add dist/ to ignored files 2025-05-11 14:15:20 +02:00
078948fda1 feat(ci): add workflows for automated releases and nightly builds
- Add workflows for nightly builds and automated releases using Gitea
- Introduce scripts for version management, release ID retrieval, and
  asset uploads
- Update changelog generation and conflict handling mechanisms
- Improve release automation with semantic versioning support
2025-05-11 14:14:59 +02:00
ee66a0ed2c chore(vscode): customize activity bar and theme settings
- Add custom color settings for the activity bar and badges
- Include a peacock theme color for workspace customization
2025-05-11 14:13:54 +02:00
25 changed files with 1219 additions and 394 deletions

50
.gitea/COMMIT_GPT.md Normal file
View File

@@ -0,0 +1,50 @@
# GPT Prompt
This is a prompt for a GPT model to generate a commit message based on the changes made in the code.
```plaintext
# System
You are a highly precise Git commit message generator for changelog-driven repositories.
Your only task is to read the provided multi-file git diff and generate **one and only one** Conventional Commit message that strictly complies with the rules below.
# Language
You MUST write the entire message in English only.
# Commit Rules
1. Use exactly one of the following lowercase types:
feat, fix, docs, style, refactor, perf, test, chore, revert
2. Optionally add a `(<scope>)` after the type:
• Use a short lowercase identifier (e.g., directory, module, file group)
• Omit if no dominant scope exists
3. Follow this structure:
`type(scope): summary`
• The summary must be in imperative mood
• Max 72 characters
• No trailing period
• Must not contain emojis or casing deviations
4. If the body is needed:
• Add exactly one blank line after the subject
• Use `- ` bullet points
• Each line must be ≤ 72 characters
• Explain what was changed and (if shown in the diff) why
5. Do NOT include:
• `BREAKING CHANGE:` or any `!` markers
• PR numbers, issue IDs, author names, ticket references
• Markdown formatting, emojis, or code blocks
6. The following `chore(...)` patterns MUST be ignored and skipped from changelogs:
chore(changelog), chore(version), chore(release): prepare for, chore(deps), chore(pr), chore(pull)
7. If the commit body contains the word `security`, it will be classified as security-relevant (🛡️), regardless of type.
8. Output MUST consist only of the final commit message, as plain text.
No extra prose, no explanation, no formatting, no examples.
# Now, read the diff and generate the message.
Output only the valid Conventional Commit message, exactly.
```

198
.gitea/HOWTO_RELEASE.md Normal file
View File

@@ -0,0 +1,198 @@
# 📦 HOWTO: Release erstellen mit Auto-Changelog-Workflow
Dieses Repository nutzt einen automatisierten CI/CD-Workflow zur **Versionsverwaltung, Changelog-Generierung und Release-Erstellung**.
Der gesamte Prozess ist deklarativ und läuft automatisch – ausgelöst durch Änderungen an einer Datei: `VERSION`.
---
## 🧭 Was passiert automatisch?
Sobald Änderungen in `main` landen, prüft der Workflow:
- 🔍 **Hat sich die Datei `VERSION` geändert?**
-**Nein** → es wird nur das `CHANGELOG.md` aktualisiert (unreleased Abschnitt)
-**Ja** → es wird:
- ein vollständiger Changelog für diese Version erzeugt
- ein Git-Tag `vX.Y.Z` erstellt
- ein Release in Gitea veröffentlicht (inkl. Beschreibung aus dem Changelog)
---
## ✅ Wie erzeuge ich ein Release?
### 1. Erhöhe die Version in der Datei `VERSION`
Beispiel:
```txt
1.2.3
```
> Diese Datei muss **als eigene Commit-Änderung** erfolgen – idealerweise als letzter Commit in einem PR.
> Die Commit-Nachricht sollte mit `chore(version)` beginnen, damit dieser nicht im Changelog auftaucht.
---
### 2. Mergen in `main`
Sobald `main` den Commit mit neuer `VERSION` enthält, wird automatisch:
- das `CHANGELOG.md` regeneriert und committed
- der neue Git-Tag erstellt (`v1.2.3`)
- ein Gitea Release mit genau diesem Changelog erzeugt
---
## 🛡️ Hinweis zu Tokens & Webhooks
Damit das Release auch korrekt weitere Workflows auslösen kann (z. B. über `on: release`), ist **ein Personal Access Token notwendig**.
### 🔐 Secret: `RELEASE_PUBLISH_TOKEN`
> Lege ein Repository-Secret mit diesem Namen an.
> Es sollte ein **Gitea Personal Access Token** mit folgenden Berechtigungen sein:
- `write:repo`
- `write:release`
- idealerweise: keine Ablaufzeit
Wird dieser Token **nicht** gesetzt, fällt der Workflow auf `ACTIONS_RUNTIME_TOKEN` zurück, aber:
- Release wird trotzdem erstellt
- **⚠️ andere Workflows, die auf `release.published` reagieren, könnten nicht getriggert werden**
---
## 🧪 Debugging-Tipps
- Stelle sicher, dass `VERSION` exakt **eine gültige neue semver-Version** enthält
- Achte auf den Commit-Log: Changelog-Commits sind mit `chore(changelog):` gekennzeichnet
- Verwende nur `main` als Trigger-Zweig
---
## 🧩 Erweiterung
In `upload-assets.yml` kannst du beliebige Build-Artefakte automatisch an das Release anhängen, sobald es veröffentlicht ist.
Dafür:
- liegt das Script `.gitea/scripts/get-release-id.sh`
- sowie `.gitea/scripts/upload-asset.sh` bereit
Mehr dazu in der Datei: `.gitea/workflows/upload-assets.yml`
---
## 🧘 Best Practice
- Changelog-Generierung nie manuell ausführen
- Nur `VERSION` ändern, um ein neues Release auszulösen
- Auf `CHANGELOG.md` nie direkt committen
- Release-Daten niemals per Hand in Gitea pflegen
📎 Alles wird versioniert, automatisiert und reproduzierbar erzeugt.
---
## 🧠 Commit-Gruppierung & Changelog-Erzeugung
Der Changelog wird auf Basis definierter **Commit-Gruppen** erzeugt.
Diese Regeln sind in `cliff.toml` unter `commit_parsers` konfiguriert.
| Prefix / Muster | Gruppe | Beschreibung |
|-------------------------------|---------------------------|--------------------------------------------------|
| `feat:` | 🚀 Features | Neue Funktionalität |
| `fix:` | 🐛 Bug Fixes | Fehlerbehebungen |
| `doc:` | 📚 Documentation | Änderungen an Doku, Readmes etc. |
| `perf:` | ⚡ Performance | Leistungsverbesserungen |
| `refactor:` | 🚜 Refactor | Reorganisation ohne Verhaltensänderung |
| `style:` | 🎨 Styling | Formatierung, Whitespaces, Code-Style |
| `test:` | 🧪 Testing | Neue oder angepasste Tests |
| `ci:` oder `chore:` (ohne Spezifizierung) | ⚙️ Miscellaneous Tasks | CI-Änderungen, Aufgaben, Wartung etc. |
| `chore(changelog)`, `chore(version)`, `chore(release): prepare for`, `chore(deps...)`, `chore(pr)`, `chore(pull)` | *(ignoriert)* | Diese Commits werden im Changelog **ausgelassen** |
| Commit-Body enthält `security` | 🛡️ Security | Sicherheitsrelevante Änderungen |
| `revert:` | ◀️ Revert | Rückgängig gemachte Commits |
| alles andere | 💼 Other | Fallback für nicht erkannte Formate |
### ✍️ Beispiel:
```bash
git commit -m "feat: add login endpoint"
git commit -m "fix: prevent crash on null input"
git commit -m "chore(version): bump to 1.2.3"
```
> Nur die ersten beiden erscheinen im Changelog – der dritte wird **automatisch übersprungen**.
---
## 🧾 Umgang mit `CHANGELOG.md` beim Mergen und Releasen
Wenn du automatisiert einen Changelog mit `git-cliff` erzeugst, ist `CHANGELOG.md` ein **generiertes Artefakt** – und kein handgepflegter Quelltext.
Beim Mergen von Feature-Branches in `main` kann es deshalb zu **unnötigen Konflikten** in dieser Datei kommen, obwohl der Inhalt später sowieso neu erzeugt wird.
---
## 🧼 Umgang mit `CHANGELOG.md` in Feature-Branches
Wenn du mit **Feature-Branches** arbeitest, wird `CHANGELOG.md` dort oft automatisch erzeugt.
Das kann beim späteren Merge in `main` zu **unnötigen Merge-Konflikten** führen.
### ✅ Empfohlene Vorgehensweise
**Bevor du den Branch mit `main` zusammenführst** (Merge oder Cherry-Pick):
```bash
git rm CHANGELOG.md
git commit -m "chore(changelog): remove generated CHANGELOG.md before merge"
git push
```
Dadurch:
* verhinderst du Merge-Konflikte mit `CHANGELOG.md`
* wird die Datei bei Feature-Branches nicht mehr automatisch erzeugt
* bleibt deine Historie sauber und konfliktfrei
> 💡 Der Workflow erzeugt `CHANGELOG.md` automatisch **nur**, wenn:
>
> * die Datei schon vorhanden ist **oder**
> * der Branch `main` heißt
---
## 🧩 Merge-Konflikte verhindern mit `.gitattributes`
Damit Git bei Konflikten in `CHANGELOG.md` **automatisch deine Version bevorzugt**, kannst du folgende Zeile in die Datei `.gitattributes` aufnehmen:
```gitattributes
CHANGELOG.md merge=ours
```
Das bedeutet:
* Beim Merge wird die Version aus dem aktuellen Branch (`ours`) behalten
* Änderungen aus dem Ziel-Branch (`theirs`) werden verworfen
### ✅ So verwendest du es richtig:
1. **Füge die Regel in `main` hinzu**:
```bash
echo "CHANGELOG.md merge=ours" >> .gitattributes
git add .gitattributes
git commit -m "chore(git): prevent merge conflicts in CHANGELOG.md"
git push origin main
```
2. **Hole sie in deinen Feature-Branch**:
```bash
git checkout feature/xyz
git rebase origin/main
```
3. **Ab sofort werden Konflikte in `CHANGELOG.md` automatisch aufgelöst** – lokal.
> ⚠️ Hinweis: Plattformen wie **Gitea, GitHub oder GitLab ignorieren `.gitattributes` beim Merge über die Web-Oberfläche**.
> Führe Merges daher **lokal** durch, wenn du Konflikte verhindern willst.

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
# cleanup_dev_versions.sh - Delete old PyPI dev versions from Gitea package registry
# Required environment variables
USERNAME="${TWINE_USERNAME}"
TOKEN="${TWINE_PASSWORD}"
REPO="${GITHUB_REPOSITORY}" # e.g., maxp/repocat
API_BASE="${GITHUB_API_URL%/}" # Strip trailing slash if present
OWNER="${REPO%%/*}"
PACKAGE_NAME="${REPO##*/}"
API_URL="${API_BASE}/packages/${OWNER}/pypi/${PACKAGE_NAME}"
# Fetch the list of versions
response=$(curl -s -u "$USERNAME:$TOKEN" "$API_URL")
# Extract all .dev versions, sort by creation time
mapfile -t versions_to_delete < <(echo "$response" | jq -r '
map(select(.version | test("\\.dev"))) |
sort_by(.created_at) |
.[0:-1][] |
.version')
# Determine latest version to keep
latest_version=$(echo "$response" | jq -r '
map(select(.version | test("\\.dev"))) |
sort_by(.created_at) |
last.version')
if [[ -z "$latest_version" || ${#versions_to_delete[@]} -eq 0 ]]; then
echo "No old .dev versions to delete."
exit 0
fi
echo "Keeping latest .dev version: $latest_version"
# Delete old .dev versions
for version in "${versions_to_delete[@]}"; do
echo "Deleting old .dev version: $version"
curl -s -X DELETE -u "$USERNAME:$TOKEN" "$API_URL/$version"
done
echo "Cleanup complete."

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
# Eingaben
TAG="$1"
TOKEN="${ACTIONS_RUNTIME_TOKEN:-<fallback_token>}"
REPO="${GITHUB_REPOSITORY:-owner/example}"
API="${GITHUB_API_URL:-https://gitea.example.tld/api/v1}"
OWNER=$(echo "$REPO" | cut -d/ -f1)
NAME=$(echo "$REPO" | cut -d/ -f2)
RESPONSE=$(curl -sf \
-H "Authorization: token $TOKEN" \
"$API/repos/$OWNER/$NAME/releases/tags/$TAG")
RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id')
echo "Release-ID für $TAG ist: $RELEASE_ID"
# Für GitHub Actions als Umgebungsvariable
echo "GT_RELEASE_ID=$RELEASE_ID" >> "$GITHUB_ENV"

View File

@@ -0,0 +1,14 @@
#!/bin/bash
BASE_VERSION=$(cat VERSION)
NIGHTLY_SUFFIX=""
if [[ "$1" == "nightly" ]]; then
# Beispiel: 20240511.1358 → 11. Mai, 13:58 Uhr
NIGHTLY_SUFFIX=".dev$(date +%Y%m%d%H%M)"
fi
FULL_VERSION="${BASE_VERSION}${NIGHTLY_SUFFIX}"
echo "Using version: $FULL_VERSION"
poetry version "$FULL_VERSION"

40
.gitea/scripts/upload-asset.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
# Eingabeparameter
FILE_PATH="$1" # z. B. ./dist/build.zip
CUSTOM_NAME="${2:-}" # optional: anderer Name unter dem das Asset gespeichert werden soll
RELEASE_ID="${GT_RELEASE_ID:-}" # aus Umgebung
# Validierung
if [[ -z "$RELEASE_ID" ]]; then
echo "❌ RELEASE_ID ist nicht gesetzt. Abbruch."
exit 1
fi
if [[ ! -f "$FILE_PATH" ]]; then
echo "❌ Datei '$FILE_PATH' existiert nicht. Abbruch."
exit 1
fi
# Default-Konfiguration
TOKEN="${ACTIONS_RUNTIME_TOKEN:-<fallback_token>}"
REPO="${GITHUB_REPOSITORY:-owner/example}"
API="${GITHUB_API_URL:-https://gitea.example.tld/api/v1}"
# Owner/Repo auflösen
OWNER=$(echo "$REPO" | cut -d/ -f1)
NAME=$(echo "$REPO" | cut -d/ -f2)
# Dateiname setzen
FILENAME="${CUSTOM_NAME:-$(basename "$FILE_PATH")}"
echo "🔼 Uploading '$FILE_PATH' as '$FILENAME' to release ID $RELEASE_ID"
# Upload
curl -sf -X POST \
-H "Authorization: token $TOKEN" \
-F "attachment=@$FILE_PATH" \
"$API/repos/$OWNER/$NAME/releases/$RELEASE_ID/assets?name=$FILENAME"
echo "✅ Upload abgeschlossen: $FILENAME"

View File

@@ -1,10 +1,12 @@
name: Build and Publish name: Build and Publish nightly package
on: on:
workflow_dispatch:
push: push:
paths: branches:
- "pyproject.toml" # Nur bei Änderungen an dieser Datei - main
workflow_dispatch: # Manuelles Anstoßen zulassen paths-ignore:
- 'CHANGELOG.md'
jobs: jobs:
build-and-publish: build-and-publish:
@@ -36,6 +38,9 @@ jobs:
run: | run: |
poetry install poetry install
- name: Set version from VERSION file (with nightly suffix)
run: ./.gitea/scripts/set_poetry_version.sh nightly
- name: Build Package - name: Build Package
working-directory: . working-directory: .
run: | run: |
@@ -48,3 +53,10 @@ jobs:
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
run: | run: |
poetry run twine upload --repository-url ${{ secrets.TWINE_URL }} dist/* poetry run twine upload --repository-url ${{ secrets.TWINE_URL }} dist/*
- name: Cleanup old dev versions
run: |
.gitea/scripts/cleanup_versions.sh '\.dev'
env:
TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}

View File

@@ -0,0 +1,64 @@
name: Build and Publish nightly package
on:
release:
types: [published]
jobs:
build-and-publish:
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: 🐍 Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: 🔄 Restore cache
uses: https://git.0xmax42.io/actions/cache@v1
with:
key: poetry-v1-${{ runner.os }}-${{ hashFiles('poetry.lock') }}
paths: |
~/.cache/pypoetry
~/.cache/pip
- name: Install Poetry
run: |
pip install poetry
- name: Install Project Dependencies
working-directory: .
run: |
poetry install
- name: Build Package
working-directory: .
run: |
poetry build
- name: Get built wheel filename
id: get_whl
run: |
echo "whl_file=$(basename dist/*.whl)" >> $GITHUB_OUTPUT
echo "sdist_file=$(basename dist/*.tar.gz)" >> $GITHUB_OUTPUT
- name: Publish to Gitea Package Registry
working-directory: .
env:
TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
run: |
poetry run twine upload --repository-url ${{ secrets.TWINE_URL }} dist/*
- name: Get Release ID from tag
run: .gitea/scripts/get-release-id.sh "${{ github.event.release.tag_name }}"
- name: Upload assets
run: |
.gitea/scripts/upload-asset.sh ./dist/${{ steps.get_whl.outputs.whl_file }}
.gitea/scripts/upload-asset.sh ./dist/${{ steps.get_whl.outputs.sdist_file }}

View File

@@ -0,0 +1,18 @@
name: Auto Changelog & (Release)
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Release
uses: https://git.0xmax42.io/actions/auto-changelog-release-action@v1
with:
token: ${{ secrets.RELEASE_PUBLISH_TOKEN }}

2
.gitignore vendored
View File

@@ -1 +1,3 @@
*__pycache__* *__pycache__*
dist/
**/.env

11
.vscode/settings.json vendored
View File

@@ -2,5 +2,14 @@
"python.envFile": "${workspaceFolder}/.env", "python.envFile": "${workspaceFolder}/.env",
"python.analysis.extraPaths": [ "python.analysis.extraPaths": [
"src" "src"
] ],
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#a9e3ea",
"activityBar.background": "#a9e3ea",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#d965cd",
"activityBarBadge.foreground": "#15202b"
},
"peacock.color": "#80d5e0"
} }

78
CHANGELOG.md Normal file
View File

@@ -0,0 +1,78 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.4.0](https://git.0xmax42.io/maxp/repoCat/compare/v0.3.0..v0.4.0) - 2025-11-27
### 🚀 Features
- *(archive)* Enable multithreaded zstd compression - ([31c3f7d](https://git.0xmax42.io/maxp/repoCat/commit/31c3f7d43880210f8056e4484b441e5f30500184))
### 🐛 Bug Fixes
- *(archive)* Skip categories with zero or unset days config - ([39f01b2](https://git.0xmax42.io/maxp/repoCat/commit/39f01b266f9d58509a6fb69fcd0734a85b2fd0ff))
## [0.3.0](https://git.0xmax42.io/maxp/repoCat/compare/v0.2.0..v0.3.0) - 2025-11-27
### 🚀 Features
- *(clean)* Add snapshot support for volatile repo cleanup - ([7f629fd](https://git.0xmax42.io/maxp/repoCat/commit/7f629fd752d7301c6c84d8227918853dd0538ff5))
- *(workflows)* Add cleanup step for old dev versions - ([aff319c](https://git.0xmax42.io/maxp/repoCat/commit/aff319cc5f5c8f954367a6a6b89c84ece6b19f4e))
- *(scripts)* Add script to clean old PyPI dev versions - ([fc8b885](https://git.0xmax42.io/maxp/repoCat/commit/fc8b885214b1f5b1d9dcff20612a583aca89f6f9))
### 📚 Documentation
- Fix typo in German documentation comment - ([801c8e2](https://git.0xmax42.io/maxp/repoCat/commit/801c8e24a139f36f714bada00e5308efb848d4f4))
### ⚙️ Miscellaneous Tasks
- *(scripts)* Update file mode to make script executable - ([c2013fc](https://git.0xmax42.io/maxp/repoCat/commit/c2013fc20538a377fc5de45bd384b6ac438fb61c))
- *(scripts)* Rename cleanup script for broader usage - ([1163744](https://git.0xmax42.io/maxp/repoCat/commit/1163744af9a3d908d6e7dbb716818bd67ec76a63))
- *(gitignore)* Update ignored files for environment and build - ([7067f08](https://git.0xmax42.io/maxp/repoCat/commit/7067f0872bdd8b456d52b3f9dab4834fd0798977))
## [0.2.0](https://git.0xmax42.io/maxp/repoCat/compare/v0.1.0..v0.2.0) - 2025-05-11
### 🚀 Features
- *(deps)* Add click dependency and update poetry lockfile - ([ef6ac68](https://git.0xmax42.io/maxp/repoCat/commit/ef6ac68f4b8a7423c4015d893647d704c0544f2c))
- *(cli)* Add verbose option to status command - ([719f257](https://git.0xmax42.io/maxp/repoCat/commit/719f2577390ed5b374d240fe6eecd527b31c587a))
- *(models)* Add catalog and filesystem utilities - ([eb5bf52](https://git.0xmax42.io/maxp/repoCat/commit/eb5bf52f2fd00d07a1642c67ff65671a4479108f))
### 🐛 Bug Fixes
- *(scripts)* Update nightly version suffix format - ([c596f46](https://git.0xmax42.io/maxp/repoCat/commit/c596f46389c2ea88dbf5699fc41a5d7107827d55))
### 🚜 Refactor
- *(cli)* Streamline repository status logic - ([55e79d2](https://git.0xmax42.io/maxp/repoCat/commit/55e79d224fb8eed83a0d5e4e1256f4f78d4a5390))
- *(review)* Streamline repo review logic and data handling - ([68b3233](https://git.0xmax42.io/maxp/repoCat/commit/68b3233fcb97b424efa565b938451640dd31b1d2))
- *(cli)* Use RepoCatalogState for repository operations - ([9bff626](https://git.0xmax42.io/maxp/repoCat/commit/9bff626f3062eb0413735e170c0a346f0946a84d))
- *(cli)* Simplify repository cleaning logic - ([458d965](https://git.0xmax42.io/maxp/repoCat/commit/458d965062498bbf4b027243f2ab0ffe7636c456))
- *(archive)* Streamline repository archiving logic - ([d3ee7ee](https://git.0xmax42.io/maxp/repoCat/commit/d3ee7ee63d2a0319243f826eea3b98fd95a6b9a6))
## [0.1.0] - 2025-05-11
### 🚀 Features
- *(ci)* Add workflows for automated releases and nightly builds - ([078948f](https://git.0xmax42.io/maxp/repoCat/commit/078948fda1f7c17b7eec25b1cc947ee0f51f1430))
- *(workflows)* Add cache restore step to build process - ([81459d6](https://git.0xmax42.io/maxp/repoCat/commit/81459d631f8dc1aff1cac6f992b32ba081f95d43))
- *(poetry)* Add twine to development dependencies - ([bf727a8](https://git.0xmax42.io/maxp/repoCat/commit/bf727a8541aa145811e9b5229e3a07f56b3393cc))
- *(ci)* Add GitHub Actions workflow for build and publish - ([880a54d](https://git.0xmax42.io/maxp/repoCat/commit/880a54d2c22414bde983610ecfa58c3c971f9f5a))
- *(cli)* Add initial repository management commands - ([ef2bac4](https://git.0xmax42.io/maxp/repoCat/commit/ef2bac4e88af9ad53f566d95b46bee473e4d0e89))
- *(vscode)* Add Python environment and analysis settings - ([d1e246c](https://git.0xmax42.io/maxp/repoCat/commit/d1e246c86853d64b7ff44cf9fb86b6bae463c85c))
- *(poetry)* Add pyproject.toml for project dependencies and config - ([4ce7eed](https://git.0xmax42.io/maxp/repoCat/commit/4ce7eedb7bac11d4500df5a62e594ddd33be8def))
### 🐛 Bug Fixes
- *(workflows)* Revert cache action version and simplify cache key - ([134c934](https://git.0xmax42.io/maxp/repoCat/commit/134c93418f63e7299d67bf5dd8689343ed0bd746))
### 📚 Documentation
- Add .gitignore and empty README.md - ([3701550](https://git.0xmax42.io/maxp/repoCat/commit/370155077d2b3f0b75b81c1527a654f2984771af))
### ⚙️ Miscellaneous Tasks
- *(gitignore)* Add dist/ to ignored files - ([66a5ddd](https://git.0xmax42.io/maxp/repoCat/commit/66a5ddd1b043ffc5c47723feec961ad77e5f82be))
- *(vscode)* Customize activity bar and theme settings - ([ee66a0e](https://git.0xmax42.io/maxp/repoCat/commit/ee66a0ed2c5855559b0ccc2706b829d734fde83d))

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.4.0

104
cliff.toml Normal file
View File

@@ -0,0 +1,104 @@
# CLIFF_VERSION=2.8.0
# git-cliff ~ default configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[remote.gitea]
owner = "maxp"
repo = "repoCat"
[changelog]
# postprocessors
postprocessors = [
{ pattern = '<GITEA_URL>', replace = "https://git.0xmax42.io" }, # replace gitea url
]
# template for the changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{%- macro remote_url() -%}
<GITEA_URL>/{{ remote.gitea.owner }}/{{ remote.gitea.repo }}
{%- endmacro -%}
{% if version %}\
{% if previous.version %}\
## [{{ version | trim_start_matches(pat="v") }}]\
({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif %}\
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }} - \
([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% endfor %}
{% endfor %}\n
"""
# template for the changelog footer
footer = """
"""
# remove the leading and trailing s
trim = true
# render body even when there are no releases to process
# render_always = true
# output file path
# output = "test.md"
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# Replace issue numbers
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit with https://github.com/crate-ci/typos
# If the spelling is incorrect, it will be automatically fixed.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(changelog\\)", skip = true },
{ message = "^chore\\(version\\)", skip = true },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps.*\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
{ message = ".*", group = "<!-- 10 -->💼 Other" },
]
# Regex to select git tags that represent releases.
tag_pattern = "v[0-9]+\\.[0-9]+\\.[0-9]+"
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

266
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
@@ -107,104 +107,104 @@ pycparser = "*"
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.1" version = "3.4.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"},
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"},
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"},
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"},
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"},
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"},
{file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"},
{file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"},
{file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"},
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"},
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"},
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"},
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"},
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"},
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"},
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"},
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"},
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"},
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"},
{file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"},
{file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"},
{file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"},
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"},
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"},
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"},
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"},
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"},
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"},
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"},
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"},
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"},
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"},
{file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"},
{file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"},
{file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"},
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"},
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"},
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"},
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"},
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"},
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"},
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"},
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"},
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"},
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"},
{file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"},
{file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"},
{file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"},
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"},
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"},
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"},
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"},
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"},
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"},
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"},
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"},
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"},
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"},
{file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"},
{file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"},
{file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"},
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"},
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"},
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"},
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"},
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"},
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"},
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"},
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"},
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"},
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"},
{file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"},
{file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"},
{file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"},
{file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"},
] ]
[[package]] [[package]]
@@ -237,48 +237,50 @@ files = [
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "44.0.2" version = "44.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false optional = false
python-versions = "!=3.9.0,!=3.9.1,>=3.7" python-versions = "!=3.9.0,!=3.9.1,>=3.7"
groups = ["dev"] groups = ["dev"]
markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\""
files = [ files = [
{file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"}, {file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"},
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"}, {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"},
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"}, {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"},
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"}, {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"},
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"}, {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"},
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"}, {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"},
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"}, {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"},
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"}, {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"},
{file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"}, {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"},
{file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"}, {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"},
{file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"}, {file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"},
{file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"}, {file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"},
{file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"}, {file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"},
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"}, {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"},
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"}, {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"},
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"}, {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"},
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"}, {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"},
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"}, {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"},
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"}, {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"},
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"}, {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"},
{file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"}, {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"},
{file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"}, {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"},
{file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"}, {file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"},
{file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"}, {file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"},
{file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"}, {file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"},
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"}, {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"},
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"}, {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"},
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"}, {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"},
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"}, {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"},
{file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"}, {file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"},
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"}, {file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"},
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"}, {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"},
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"}, {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"},
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"}, {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"},
{file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"}, {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"},
{file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"},
] ]
[package.dependencies] [package.dependencies]
@@ -291,7 +293,7 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
sdist = ["build (>=1.0.0)"] sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"] ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"] test-randomorder = ["pytest-randomly"]
[[package]] [[package]]
@@ -1108,4 +1110,4 @@ cffi = ["cffi (>=1.11)"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.12" python-versions = ">=3.12"
content-hash = "413f1d8377278d9377ecc6f9018f23d3b0bda29587689a3fc5946d89ecd99631" content-hash = "101746662448c2be59e530e0f6437825338181f70e8466275317f1973cf9209b"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "repocat" name = "repocat"
version = "0.1.0" version = "0.4.0"
description = "" description = ""
authors = ["Max P. <Mail@MPassarello.de>"] authors = ["Max P. <Mail@MPassarello.de>"]
readme = "README.md" readme = "README.md"
@@ -10,6 +10,7 @@ packages = [{ include = "repocat", from = "src" }]
python = ">=3.12" python = ">=3.12"
rich = "^14.0.0" rich = "^14.0.0"
typer = { extras = ["all"], version = "^0.15.3" } typer = { extras = ["all"], version = "^0.15.3" }
click = "<8.2.0"
pyyaml = "^6.0.2" pyyaml = "^6.0.2"
pydantic = "^2.11.4" pydantic = "^2.11.4"
zstandard = "^0.23.0" zstandard = "^0.23.0"

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
# Stelle sicher, dass wir im Projektverzeichnis sind
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PYPROJECT="pyproject.toml"
VERSION_FILE="VERSION"
# Extrahiere die Version mit grep + sed (keine externen Abhängigkeiten nötig)
VERSION=$(grep -E '^version\s*=' "$PYPROJECT" | head -n1 | sed -E 's/.*=\s*"([^"]+)".*/\1/')
if [[ -z "$VERSION" ]]; then
echo "❌ Version konnte nicht aus $PYPROJECT gelesen werden."
exit 1
fi
printf "%s" "$VERSION" > "$VERSION_FILE"
echo "✅ Version synchronisiert: $VERSION$VERSION_FILE"

View File

@@ -1,29 +1,21 @@
import shutil import shutil
import tarfile import tarfile
from datetime import datetime, timedelta from datetime import datetime
from pathlib import Path from pathlib import Path
from typer import Typer, Option from typer import Typer, Option
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
import subprocess
import zstandard import zstandard
import uuid import uuid
from repocat.config import load_config from repocat.config import load_config
from repocat.cli.clean import is_git_dirty, is_git_unpushed, get_dir_size from repocat.models.catalog import RepoCatalogState
app = Typer() app = Typer()
console = Console() console = Console()
def get_archive_target_dir(): def archive_repo(source: Path, target_dir: Path, dry_run: bool) -> Path:
config = load_config()
for cat in config.categories:
if cat.is_archive:
return config.base_dir / cat.subdir
raise ValueError("Keine Archiv-Kategorie (is_archive: true) in der Konfiguration gefunden.")
def archive_repo(source: Path, target_dir: Path, dry_run: bool):
today = datetime.today().strftime("%Y.%m.%d") today = datetime.today().strftime("%Y.%m.%d")
unique = uuid.uuid4().hex[:8] unique = uuid.uuid4().hex[:8]
archive_name = f"{today} - {source.name} - {unique}.tar.zst" archive_name = f"{today} - {source.name} - {unique}.tar.zst"
@@ -33,10 +25,10 @@ def archive_repo(source: Path, target_dir: Path, dry_run: bool):
raise FileExistsError(f"Archiv existiert bereits: {archive_path}") raise FileExistsError(f"Archiv existiert bereits: {archive_path}")
if dry_run: if dry_run:
return archive_path # Nur anzeigen return archive_path
with open(archive_path, "wb") as f: with open(archive_path, "wb") as f:
cctx = zstandard.ZstdCompressor(level=20) cctx = zstandard.ZstdCompressor(level=20, threads=-1)
with cctx.stream_writer(f) as compressor: with cctx.stream_writer(f) as compressor:
with tarfile.open(fileobj=compressor, mode="w|") as tar: with tarfile.open(fileobj=compressor, mode="w|") as tar:
tar.add(source, arcname=source.name) tar.add(source, arcname=source.name)
@@ -44,6 +36,7 @@ def archive_repo(source: Path, target_dir: Path, dry_run: bool):
shutil.rmtree(source) shutil.rmtree(source)
return archive_path return archive_path
@app.command("run") @app.command("run")
def run( def run(
older_than: int = Option(30, "--older-than", help="Nur Repositories älter als X Tage archivieren."), older_than: int = Option(30, "--older-than", help="Nur Repositories älter als X Tage archivieren."),
@@ -51,11 +44,16 @@ def run(
category: list[str] = Option(None, "--category", "-c", help="Nur bestimmte Kategorien prüfen."), category: list[str] = Option(None, "--category", "-c", help="Nur bestimmte Kategorien prüfen."),
): ):
config = load_config() config = load_config()
now = datetime.now() catalog = RepoCatalogState.from_config(config)
archive_path = get_archive_target_dir()
archive_cat = next((c for c in catalog.categories if c.config.is_archive), None)
if not archive_cat:
console.print("[red]Keine Archiv-Kategorie in der Konfiguration gefunden.[/]")
raise SystemExit(1)
archive_path = archive_cat.path
archive_path.mkdir(parents=True, exist_ok=True) archive_path.mkdir(parents=True, exist_ok=True)
base = config.base_dir
table = Table(title="Archivierung") table = Table(title="Archivierung")
table.add_column("Kategorie") table.add_column("Kategorie")
table.add_column("Repository") table.add_column("Repository")
@@ -66,44 +64,36 @@ def run(
archived = 0 archived = 0
skipped = 0 skipped = 0
for cat in config.categories: for cat in catalog.categories:
if cat.is_archive or cat.name == "review" or cat.volatile: if cat.config.is_archive or cat.config.name == "review" or cat.config.volatile:
continue continue
if category and cat.name not in category: if category and cat.config.name not in category:
continue continue
source_dir = base / cat.subdir if cat.config.days == 0 or cat.config.days is None:
if not source_dir.exists():
continue continue
for repo in sorted(source_dir.iterdir()): limit_days = cat.config.days if cat.config.days is not None else older_than
if not repo.is_dir():
continue
mtime = datetime.fromtimestamp(repo.stat().st_mtime) for repo in cat.repos:
age_days = (now - mtime).days if not repo.is_expired(limit_days):
size_mb = get_dir_size(repo) / 1024 / 1024 table.add_row(cat.config.name, repo.name, str(repo.age_days), f"{repo.size_mb:.1f}", "[blue]Zu jung[/]")
limit_days = cat.days if cat.days is not None else older_than
if age_days < limit_days:
table.add_row(cat.name, repo.name, str(age_days), f"{size_mb:.1f}", "[blue]Zu jung[/]")
skipped += 1 skipped += 1
continue continue
if (repo / ".git").exists(): if repo.git and repo.git.is_protected:
if is_git_dirty(repo) or is_git_unpushed(repo): table.add_row(cat.config.name, repo.name, str(repo.age_days), f"{repo.size_mb:.1f}", "[blue]Geschützt (git)[/]")
table.add_row(cat.name, repo.name, str(age_days), f"{size_mb:.1f}", "[blue]Geschützt (git)[/]") skipped += 1
skipped += 1 continue
continue
try: try:
archive_file = archive_repo(repo, archive_path, dry_run) archive_file = archive_repo(repo.path, archive_path, dry_run)
action = "[yellow]Würde archivieren[/]" if dry_run else f"[green]Archiviert →[/] {archive_file.name}" action = "[yellow]Würde archivieren[/]" if dry_run else f"[green]Archiviert →[/] {archive_file.name}"
archived += 1 archived += 1
except Exception as e: except Exception as e:
action = f"[red]Fehler: {e}[/]" action = f"[red]Fehler: {e}[/]"
table.add_row(cat.name, repo.name, str(age_days), f"{size_mb:.1f}", action) table.add_row(cat.config.name, repo.name, str(repo.age_days), f"{repo.size_mb:.1f}", action)
console.print(table) console.print(table)
console.print(f"\n[green]✓ Archivierung abgeschlossen[/] – {archived} archiviert, {skipped} übersprungen") console.print(f"\n[green]✓ Archivierung abgeschlossen[/] – {archived} archiviert, {skipped} übersprungen")

View File

@@ -1,65 +1,40 @@
import subprocess from typer import Typer, Option
from typer import Typer, Option, Argument
from pathlib import Path
import shutil
from datetime import datetime, timedelta
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
import shutil
import os import os
from datetime import datetime
from repocat.config import load_config from repocat.config import load_config
from repocat.models.catalog import RepoCatalogState
app = Typer() app = Typer()
console = Console() console = Console()
def is_git_dirty(path: Path) -> bool:
try:
result = subprocess.run(
["git", "status", "--porcelain"],
cwd=path,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
return bool(result.stdout.strip())
except Exception:
return False
def is_git_unpushed(path: Path) -> bool:
try:
# Check if upstream exists
subprocess.run(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
cwd=path, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
result = subprocess.run(
["git", "log", "@{u}..HEAD", "--oneline"],
cwd=path,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
return bool(result.stdout.strip())
except Exception:
return False
def get_dir_size(path: Path) -> int:
"""Returns size in bytes."""
return sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
def complete_categories(incomplete: str): def complete_categories(incomplete: str):
config = load_config() config = load_config()
return [c.name for c in config.categories if c.name.startswith(incomplete)] return [c.name for c in config.categories if c.name.startswith(incomplete)]
@app.command("volatile") @app.command("volatile")
def clean_volatile( def clean_volatile(
dry_run: bool = Option(False, "--dry-run", help="Nur anzeigen, was gelöscht würde"), dry_run: bool = Option(False, "--dry-run", help="Nur anzeigen, was gelöscht würde"),
category: list[str] = Option(None, "--category", "-c", help="Nur bestimmte Kategorien reinigen", autocompletion=complete_categories), category: list[str] = Option(None, "--category", "-c", help="Nur bestimmte Kategorien reinigen", autocompletion=complete_categories),
): ):
""" """
Löscht **bedingungslos** alle Repositories aus `volatile`-Kategorien (z.B. `scratches`). Löscht **bedingungslos** alle Repositories aus `volatile`-Kategorien (z.B. `scratches`).
""" """
config = load_config() config = load_config()
base = config.base_dir catalog = RepoCatalogState.from_config(config)
# Snapshot configuration
use_snapshots = config.defaults.snapshots if config.defaults else False
snapshot_count = config.defaults.snapshot_count if config.defaults else 5
snapshot_root = config.base_dir / ".snapshots"
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
current_snapshot_dir = snapshot_root / timestamp
table = Table(title="Clean – Volatile (Zerstörung)") table = Table(title="Clean – Volatile (Zerstörung)")
table.add_column("Kategorie") table.add_column("Kategorie")
@@ -69,37 +44,87 @@ def clean_volatile(
deleted = 0 deleted = 0
skipped = 0 skipped = 0
snapshotted_categories = set()
for cat in config.categories: for cat in catalog.categories:
if not cat.volatile: if not cat.config.volatile:
continue continue
if category and cat.name not in category: if category and cat.config.name not in category:
continue continue
path = base / cat.subdir # If snapshots are enabled, we handle the whole category at once
if not path.exists(): if use_snapshots:
continue if not cat.path.exists():
for repo in sorted(path.iterdir()):
if not repo.is_dir():
continue continue
size_mb = get_dir_size(repo) / 1024 / 1024
if dry_run: if dry_run:
action = "[yellow]Würde löschen[/]" action = f"[yellow]Würde verschieben nach .snapshots/{timestamp}[/]"
else: else:
try: try:
shutil.rmtree(repo) # Create snapshot dir only if we actually have something to move
action = "[red]Gelöscht[/]" if not current_snapshot_dir.exists():
deleted += 1 current_snapshot_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
action = f"[red]Fehler: {e}[/]"
table.add_row(cat.name, repo.name, f"{size_mb:.1f}", action) target_dir = current_snapshot_dir / cat.config.subdir
# Ensure parent of target exists (if subdir is nested)
target_dir.parent.mkdir(parents=True, exist_ok=True)
# Move the whole category folder
# Note: shutil.move(src, dst) where dst does not exist moves src to dst.
# If dst exists, it moves src inside dst.
# We want to rename cat.path to target_dir.
shutil.move(str(cat.path), str(target_dir))
# Recreate the empty category folder
cat.path.mkdir(parents=True, exist_ok=True)
action = f"[green]Verschoben nach .snapshots/{timestamp}[/]"
snapshotted_categories.add(cat.config.name)
except Exception as e:
action = f"[red]Fehler beim Verschieben: {e}[/]"
# Add rows for all repos in this category to show what happened to them
if cat.repos:
for repo in cat.repos:
table.add_row(cat.config.name, repo.name, f"{repo.size_mb:.1f}", action)
deleted += 1 # Count as "processed/removed from volatile"
else:
# If no repos but folder existed (and maybe had other files), show a generic row
table.add_row(cat.config.name, "(Alle Dateien)", "-", action)
else:
# Old behavior: Delete individual repos
for repo in cat.repos:
size_mb = repo.size_mb
if dry_run:
action = "[yellow]Würde löschen[/]"
else:
try:
shutil.rmtree(repo.path)
action = "[red]Gelöscht[/]"
deleted += 1
except Exception as e:
action = f"[red]Fehler: {e}[/]"
table.add_row(cat.config.name, repo.name, f"{size_mb:.1f}", action)
console.print(table) console.print(table)
console.print(f"\n[green]✓ Volatile-Clean abgeschlossen[/] – {deleted} gelöscht, {skipped} übersprungen")
# Prune old snapshots
if use_snapshots and not dry_run and snapshot_root.exists():
try:
snapshots = sorted([p for p in snapshot_root.iterdir() if p.is_dir()], key=lambda p: p.name)
if len(snapshots) > snapshot_count:
to_delete = snapshots[:-snapshot_count]
for snap in to_delete:
shutil.rmtree(snap)
console.print(f"[dim]Alte Snapshots bereinigt: {len(to_delete)} gelöscht[/]")
except Exception as e:
console.print(f"[red]Fehler beim Bereinigen der Snapshots: {e}[/]")
console.print(f"\n[green]✓ Volatile-Clean abgeschlossen[/] – {deleted} Repositories verarbeitet")
@app.command() @app.command()
def run( def run(
@@ -109,11 +134,7 @@ def run(
""" """
Löscht alte oder volatile Repositories aus den konfigurierten Kategorien. Löscht alte oder volatile Repositories aus den konfigurierten Kategorien.
""" """
config = load_config() catalog = RepoCatalogState.from_config(load_config())
now = datetime.now()
base = config.base_dir
console.print(f"[bold yellow]Base-Verzeichnis:[/] {base}")
table = Table(title="Clean-Ergebnis") table = Table(title="Clean-Ergebnis")
table.add_column("Kategorie") table.add_column("Kategorie")
@@ -125,43 +146,27 @@ def run(
deleted = 0 deleted = 0
skipped = 0 skipped = 0
for cat in config.categories: for cat in catalog.categories:
if category and cat.name not in category: if category and cat.config.name not in category:
continue
if cat.config.is_archive:
continue continue
if cat.is_archive: continue limit_days = cat.config.days or 0
path = base / cat.subdir
if not path.exists():
continue
cutoff = now - timedelta(days=cat.days or 0)
for repo in sorted(path.iterdir()):
if not repo.is_dir():
continue
mtime = datetime.fromtimestamp(repo.stat().st_mtime)
age_days = (now - mtime).days
size_mb = get_dir_size(repo) / 1024 / 1024
for repo in cat.repos:
should_delete = ( should_delete = (
(cat.volatile) cat.config.volatile
or (cat.days is not None and mtime < cutoff) or (cat.config.days is not None and repo.is_expired(limit_days))
) )
# Git-Schutz prüfen if repo.git and repo.git.is_protected:
is_git = (repo / ".git").exists() reasons = []
dirty = is_git and is_git_dirty(repo) if repo.git.dirty:
unpushed = is_git and is_git_unpushed(repo) reasons.append("uncommitted")
if repo.git.unpushed:
if is_git and (dirty or unpushed): reasons.append("unpushed")
reason = [] status = "/".join(reasons)
if dirty:
reason.append("uncommitted")
if unpushed:
reason.append("unpushed")
status = "/".join(reason)
action = f"[blue]Geschützt ({status})[/]" action = f"[blue]Geschützt ({status})[/]"
skipped += 1 skipped += 1
else: else:
@@ -170,7 +175,7 @@ def run(
action = "[yellow]Würde löschen[/]" action = "[yellow]Würde löschen[/]"
else: else:
try: try:
shutil.rmtree(repo) shutil.rmtree(repo.path)
action = "[red]Gelöscht[/]" action = "[red]Gelöscht[/]"
deleted += 1 deleted += 1
except Exception as e: except Exception as e:
@@ -179,7 +184,7 @@ def run(
action = "[cyan]Keine Aktion[/]" action = "[cyan]Keine Aktion[/]"
skipped += 1 skipped += 1
table.add_row(cat.name, repo.name, str(age_days), f"{size_mb:.1f}", action) table.add_row(cat.config.name, repo.name, str(repo.age_days), f"{repo.size_mb:.1f}", action)
console.print(table) console.print(table)
console.print(f"\n[green]✓ Clean abgeschlossen[/] – {deleted} gelöscht, {skipped} übersprungen") console.print(f"\n[green]✓ Clean abgeschlossen[/] – {deleted} gelöscht, {skipped} übersprungen")

View File

@@ -1,10 +1,9 @@
from typer import Typer, Argument from typer import Typer, Argument
from rich.console import Console from rich.console import Console
from pathlib import Path
import shutil
import typer import typer
from repocat.config import load_config from repocat.config import load_config
from repocat.models.catalog import RepoCatalogState
app = Typer() app = Typer()
console = Console() console = Console()
@@ -12,20 +11,16 @@ console = Console()
def complete_source_repo(incomplete: str): def complete_source_repo(incomplete: str):
config = load_config() config = load_config()
base = config.base_dir catalog = RepoCatalogState.from_config(config)
results = [] results = []
for cat in config.categories: for cat in catalog.categories:
if cat.is_archive: if cat.config.is_archive:
continue continue
cat_path = base / cat.subdir for repo in cat.repos:
if not cat_path.exists(): full_name = f"{cat.config.name}/{repo.name}"
continue if full_name.startswith(incomplete):
for item in cat_path.iterdir(): results.append(full_name)
if item.is_dir():
full_name = f"{cat.name}/{item.name}"
if full_name.startswith(incomplete):
results.append(full_name)
return sorted(results) return sorted(results)
@@ -43,39 +38,35 @@ def move_command(
Verschiebt ein Repository in eine andere Kategorie. Verschiebt ein Repository in eine andere Kategorie.
""" """
config = load_config() config = load_config()
base = config.base_dir catalog = RepoCatalogState.from_config(config)
# Quelle aufspalten in Kategorie und Repo
try: try:
source_cat_name, repo_name = source.split("/", 1) source_cat_name, repo_name = source.split("/", 1)
except ValueError: except ValueError:
console.print("[red]Ungültiges Format für Quelle. Bitte <kategorie>/<reponame> angeben.[/]") console.print("[red]Ungültiges Format für Quelle. Bitte <kategorie>/<reponame> angeben.[/]")
raise typer.Exit(1) raise typer.Exit(1)
source_cat = next((c for c in config.categories if c.name == source_cat_name), None) source_cat = catalog.get_category(source_cat_name)
if not source_cat: if not source_cat:
console.print(f"[red]Unbekannte Quellkategorie:[/] {source_cat_name}") console.print(f"[red]Unbekannte Quellkategorie:[/] {source_cat_name}")
raise typer.Exit(1) raise typer.Exit(1)
target_cat = next((c for c in config.categories if c.name == target), None) target_cat = catalog.get_category(target)
if not target_cat: if not target_cat:
console.print(f"[red]Ungültige Zielkategorie:[/] {target}") console.print(f"[red]Ungültige Zielkategorie:[/] {target}")
raise typer.Exit(1) raise typer.Exit(1)
source_path = base / source_cat.subdir / repo_name repo = source_cat.find_repo(repo_name)
destination_path = base / target_cat.subdir / repo_name if not repo:
console.print(f"[red]Repository '{repo_name}' nicht gefunden in Kategorie '{source_cat_name}'.[/]")
if not source_path.exists():
console.print(f"[red]Quell-Repository existiert nicht:[/] {source_path}")
raise typer.Exit(1)
if destination_path.exists():
console.print(f"[red]Zielordner existiert bereits:[/] {destination_path}")
raise typer.Exit(1) raise typer.Exit(1)
try: try:
shutil.move(str(source_path), str(destination_path)) repo.move_to(target_cat.path)
console.print(f"[green]✓ Erfolgreich verschoben:[/] {source_cat.name}/{repo_name}{target}") console.print(f"[green]✓ Erfolgreich verschoben:[/] {source_cat_name}/{repo_name}{target}")
except FileExistsError as e:
console.print(f"[red]{e}[/]")
raise typer.Exit(1)
except Exception as e: except Exception as e:
console.print(f"[red]Fehler beim Verschieben:[/] {e}") console.print(f"[red]Fehler beim Verschieben:[/] {e}")
raise typer.Exit(1) raise typer.Exit(1)

View File

@@ -1,19 +1,15 @@
from typer import Typer, confirm, prompt from typer import Typer
from rich.console import Console from rich.console import Console
from rich.table import Table
from pathlib import Path
import shutil
from datetime import datetime
import typer
from rich.prompt import Prompt, Confirm from rich.prompt import Prompt, Confirm
from datetime import datetime
import shutil
import typer
from repocat.cli.move import move_command
from repocat.config import load_config from repocat.config import load_config
from repocat.models.config import RepoCategory from repocat.models.catalog import RepoCatalogState
from repocat.cli.move import move_command
from repocat.cli.archive import archive_repo from repocat.cli.archive import archive_repo
from repocat.cli.clean import get_dir_size, is_git_dirty, is_git_unpushed
app = Typer() app = Typer()
console = Console() console = Console()
@@ -23,48 +19,39 @@ def review():
""" """
Führt interaktiv durch alle Repositories im Review-Ordner. Führt interaktiv durch alle Repositories im Review-Ordner.
""" """
config = load_config() catalog = RepoCatalogState.from_config(load_config())
base = config.base_dir
archive_cat = next((c for c in config.categories if c.is_archive), None) archive_cat = next((c for c in catalog.categories if c.config.is_archive), None)
if not archive_cat: if not archive_cat:
console.print("[red]Keine Archiv-Kategorie in der Konfiguration gefunden.[/]") console.print("[red]Keine Archiv-Kategorie in der Konfiguration gefunden.[/]")
raise typer.Exit(1) raise typer.Exit(1)
archive_path = base / archive_cat.subdir
archive_path.mkdir(parents=True, exist_ok=True)
review_cat = catalog.get_category("review")
review_cat = next((c for c in config.categories if c.name == "review"), None)
if not review_cat: if not review_cat:
console.print("[red]Kein 'review'-Eintrag in der Konfiguration gefunden.[/]") console.print("[red]Kein 'review'-Eintrag in der Konfiguration gefunden.[/]")
raise typer.Exit(1) raise typer.Exit(1)
review_path = base / review_cat.subdir if not review_cat.repos:
if not review_path.exists():
console.print(f"[yellow]Hinweis:[/] Kein review-Ordner vorhanden unter {review_path}")
return
repos = [p for p in review_path.iterdir() if p.is_dir()]
if not repos:
console.print("[green]✓ Keine Repositories im Review-Ordner.[/]") console.print("[green]✓ Keine Repositories im Review-Ordner.[/]")
return return
for repo in sorted(repos): for repo in sorted(review_cat.repos, key=lambda r: r.name):
console.rule(f"[bold cyan]{repo.name}") console.rule(f"[bold cyan]{repo.name}")
age_days = (datetime.now() - datetime.fromtimestamp(repo.stat().st_mtime)).days console.print(f"[bold]Alter:[/] {repo.age_days} Tage")
size_mb = get_dir_size(repo) / 1024 / 1024 console.print(f"[bold]Größe:[/] {repo.size_mb:.1f} MB")
status = [] if repo.git:
if is_git_dirty(repo): status = []
status.append("uncommitted") if repo.git.dirty:
if is_git_unpushed(repo): status.append("uncommitted")
status.append("unpushed") if repo.git.unpushed:
if not status: status.append("unpushed")
status.append("clean") if not status:
status.append("clean")
else:
status = ["kein Git"]
console.print(f"[bold]Alter:[/] {age_days} Tage")
console.print(f"[bold]Größe:[/] {size_mb:.1f} MB")
console.print(f"[bold]Git:[/] {', '.join(status)}") console.print(f"[bold]Git:[/] {', '.join(status)}")
while True: while True:
@@ -81,17 +68,17 @@ def review():
raise typer.Exit() raise typer.Exit()
elif action in ("d", "delete"): elif action in ("d", "delete"):
if Confirm.ask(f"Bist du sicher, dass du [red]{repo.name}[/] löschen willst?"): if Confirm.ask(f"Bist du sicher, dass du [red]{repo.name}[/] löschen willst?"):
shutil.rmtree(repo) shutil.rmtree(repo.path)
console.print(f"[red]✓ Gelöscht:[/] {repo.name}") console.print(f"[red]✓ Gelöscht:[/] {repo.name}")
break break
elif action in ("m", "move"): elif action in ("m", "move"):
target_categories = [c.name for c in config.categories if c.name != "review" and not c.is_archive] target_categories = [c.config.name for c in catalog.categories if c.config.name != "review" and not c.config.is_archive]
target = Prompt.ask("Zielkategorie", choices=target_categories) target = Prompt.ask("Zielkategorie", choices=target_categories)
move_command(source=f"review/{repo.name}", target=target) move_command(source=f"review/{repo.name}", target=target)
break break
elif action in ("a", "archive"): elif action in ("a", "archive"):
try: try:
archive_file = archive_repo(repo, archive_path, dry_run=False) archive_file = archive_repo(repo.path, archive_cat.path, dry_run=False)
console.print(f"[green]✓ Archiviert:[/] {archive_file.name}") console.print(f"[green]✓ Archiviert:[/] {archive_file.name}")
break break
except Exception as e: except Exception as e:

View File

@@ -1,71 +1,45 @@
from typer import Typer from typer import Typer, Option
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
from pathlib import Path
from datetime import datetime
from repocat.config import load_config from repocat.config import load_config
from repocat.models.config import RepoCategory from repocat.models.catalog import RepoCatalogState
app = Typer() app = Typer()
console = Console() console = Console()
def get_dir_size(path: Path) -> int:
"""Returns size in bytes."""
return sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
def summarize_category(category: RepoCategory, base_path: Path):
cat_path = base_path / category.subdir
if not cat_path.exists():
return 0, 0.0, None, None
now = datetime.now()
repo_infos = []
for repo in cat_path.iterdir():
if not repo.is_dir():
continue
size_bytes = get_dir_size(repo)
mtime = datetime.fromtimestamp(repo.stat().st_mtime)
age_days = (now - mtime).days
repo_infos.append((repo.name, size_bytes, age_days, mtime))
if not repo_infos:
return 0, 0.0, None, None
total_size = sum(info[1] for info in repo_infos) / 1024 / 1024
avg_age = sum(info[2] for info in repo_infos) // len(repo_infos)
oldest = max(repo_infos, key=lambda x: x[2]) # größtes Alter
return len(repo_infos), total_size, avg_age, (oldest[0], oldest[2])
@app.command("status") @app.command("status")
def status(): def status(verbose: bool = Option(False, "--verbose", "-v", help="Zeige Details zu einzelnen Repositories")):
""" """
Zeigt eine Übersicht über alle Repository-Kategorien. Zeigt eine Übersicht über alle Repository-Kategorien.
""" """
config = load_config() catalog = RepoCatalogState.from_config(load_config())
base = config.base_dir
table = Table(title="Repository-Status") table = Table(title="Repository-Status")
table.add_column("Kategorie") table.add_column("Kategorie")
table.add_column("Repos", justify="right") table.add_column("Repos", justify="right")
table.add_column("Gesamtgröße", justify="right") table.add_column("Gesamtgröße", justify="right")
table.add_column("Ø Alter", justify="right") table.add_column("\u00d8 Alter", justify="right")
table.add_column("Ältestes Repo", justify="left") table.add_column("\u00c4ltestes Repo", justify="left")
for cat in config.categories: for category in catalog.categories:
if cat.is_archive: if category.config.is_archive:
continue continue
count, total_size, avg_age, oldest = summarize_category(cat, base)
repos = category.repos
count = len(repos)
total_size = category.total_size_mb()
if not repos:
avg_age = None
oldest = None
else:
ages = [repo.age_days for repo in repos]
avg_age = sum(ages) // len(ages)
oldest_repo = max(repos, key=lambda r: r.age_days)
oldest = (oldest_repo.name, oldest_repo.age_days)
table.add_row( table.add_row(
cat.name, category.config.name,
str(count), str(count),
f"{total_size:.1f} MB", f"{total_size:.1f} MB",
f"{avg_age or ''} d", f"{avg_age or ''} d",
@@ -73,3 +47,38 @@ def status():
) )
console.print(table) console.print(table)
if verbose:
for category in catalog.categories:
if category.config.is_archive:
continue
if not category.repos:
continue
repo_table = Table(title=f"Details: {category.config.name}")
repo_table.add_column("Repository")
repo_table.add_column("Alter (Tage)", justify="right")
repo_table.add_column("Größe (MB)", justify="right")
repo_table.add_column("Git", justify="left")
for repo in sorted(category.repos, key=lambda r: r.age_days, reverse=True):
git_status = ""
if repo.git:
if repo.git.dirty:
git_status += "uncommitted "
if repo.git.unpushed:
git_status += "unpushed"
if not git_status:
git_status = "clean"
else:
git_status = ""
repo_table.add_row(
repo.name,
str(repo.age_days),
f"{repo.size_mb:.1f}",
git_status.strip()
)
console.print(repo_table)

View File

@@ -0,0 +1,118 @@
from __future__ import annotations
from pathlib import Path
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field
import shutil
from repocat.models.config import RepoCategory, RepoCatConfig
from repocat.utils.fsutils import get_dir_size, is_git_dirty, is_git_unpushed
class GitStatus(BaseModel):
dirty: bool
unpushed: bool
branch: Optional[str] = None
remote_url: Optional[str] = None
@property
def is_protected(self) -> bool:
return self.dirty or self.unpushed
class RepoModel(BaseModel):
name: str
path: Path
size_bytes: int
last_modified: datetime
git: Optional[GitStatus] = None
@property
def size_mb(self) -> float:
return self.size_bytes / 1024 / 1024
@property
def age_days(self) -> int:
return (datetime.now() - self.last_modified).days
@property
def is_git_repo(self) -> bool:
return self.git is not None
def is_expired(self, days: int) -> bool:
return self.age_days >= days
def move_to(self, target_dir: Path) -> None:
destination = target_dir / self.name
if destination.exists():
raise FileExistsError(f"Zielordner existiert bereits: {destination}")
shutil.move(str(self.path), str(destination))
self.path = destination
class RepoCategoryState(BaseModel):
config: RepoCategory
path: Path
repos: List[RepoModel] = Field(default_factory=list)
def total_size_mb(self) -> float:
return sum(repo.size_mb for repo in self.repos)
def find_repo(self, name: str) -> Optional[RepoModel]:
return next((r for r in self.repos if r.name == name), None)
class RepoCatalogState(BaseModel):
config: RepoCatConfig
categories: List[RepoCategoryState] = Field(default_factory=list)
@classmethod
def from_config(cls, config: RepoCatConfig) -> RepoCatalogState:
categories: List[RepoCategoryState] = []
for cat_cfg in config.categories:
cat_path = config.base_dir / cat_cfg.subdir
repos: List[RepoModel] = []
if cat_path.exists():
for item in sorted(cat_path.iterdir()):
if not item.is_dir():
continue
last_mod = datetime.fromtimestamp(item.stat().st_mtime)
size = get_dir_size(item)
git = None
if (item / ".git").exists():
git = GitStatus(
dirty=is_git_dirty(item),
unpushed=is_git_unpushed(item),
)
repos.append(RepoModel(
name=item.name,
path=item,
size_bytes=size,
last_modified=last_mod,
git=git,
))
categories.append(RepoCategoryState(
config=cat_cfg,
path=cat_path,
repos=repos,
))
return cls(config=config, categories=categories)
def get_category(self, name: str) -> Optional[RepoCategoryState]:
return next((c for c in self.categories if c.config.name == name), None)
def find_repo(self, name: str) -> Optional[RepoModel]:
for cat in self.categories:
if (repo := cat.find_repo(name)):
return repo
return None
def missing_directories(self) -> List[Path]:
return [cat.path for cat in self.categories if not cat.path.exists()]

View File

@@ -13,6 +13,8 @@ class RepoCategory(BaseModel):
class RepoCatDefaults(BaseModel): class RepoCatDefaults(BaseModel):
dry_run: bool = True dry_run: bool = True
auto_create_directories: bool = True auto_create_directories: bool = True
snapshots: bool = False
snapshot_count: int = 5
class RepoCatConfig(BaseModel): class RepoCatConfig(BaseModel):
base_dir: Path = Field(default=Path("~/Repositories").expanduser()) base_dir: Path = Field(default=Path("~/Repositories").expanduser())

View File

@@ -0,0 +1,43 @@
import subprocess
from pathlib import Path
def get_dir_size(path: Path) -> int:
"""Returns directory size in bytes."""
return sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
def is_git_dirty(path: Path) -> bool:
"""Returns True if there are uncommitted changes."""
try:
result = subprocess.run(
["git", "status", "--porcelain"],
cwd=path,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
return bool(result.stdout.strip())
except Exception:
return False
def is_git_unpushed(path: Path) -> bool:
"""Returns True if there are commits that haven't been pushed to upstream."""
try:
# Check if upstream exists
subprocess.run(
["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
cwd=path,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
result = subprocess.run(
["git", "log", "@{u}..HEAD", "--oneline"],
cwd=path,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
return bool(result.stdout.strip())
except Exception:
return False