[INIT-001] Initial project setup with Clean Architecture (feat)
Some checks failed
CD - Build & Deploy / build-and-push (push) Has been cancelled
CD - Build & Deploy / package-helm (push) Has been cancelled
CD - Build & Deploy / deploy-staging (push) Has been cancelled
CD - Build & Deploy / deploy-production (push) Has been cancelled
CD - Build & Deploy / release (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / security (push) Has been cancelled
Some checks failed
CD - Build & Deploy / build-and-push (push) Has been cancelled
CD - Build & Deploy / package-helm (push) Has been cancelled
CD - Build & Deploy / deploy-staging (push) Has been cancelled
CD - Build & Deploy / deploy-production (push) Has been cancelled
CD - Build & Deploy / release (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / security (push) Has been cancelled
- Implemented Clean Architecture with Domain, Application, Infrastructure, Presentation layers - Added comprehensive project structure following SOLID principles - Created Kubernetes deployment with Helm charts (HPA, PDB, NetworkPolicy) - Configured ArgoCD for automated deployment (production + staging) - Implemented CI/CD pipeline with GitHub Actions - Added comprehensive documentation (handbook, architecture, coding standards) - Configured PostgreSQL, Redis, Celery for backend services - Created modern landing page with Persian fonts (Vazirmatn) - Added Docker multi-stage build for production - Configured development tools (pytest, black, flake8, mypy, isort) - Added pre-commit hooks for code quality - Implemented Makefile for common operations
This commit is contained in:
193
.github/workflows/cd.yml
vendored
Normal file
193
.github/workflows/cd.yml
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
name: CD - Build & Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
outputs:
|
||||
image-tag: ${{ steps.meta.outputs.tags }}
|
||||
image-version: ${{ steps.meta.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,prefix={{branch}}-
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Image digest
|
||||
run: echo "Image pushed with digest ${{ steps.build.outputs.digest }}"
|
||||
|
||||
package-helm:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-push
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: 'latest'
|
||||
|
||||
- name: Package Helm chart
|
||||
run: |
|
||||
helm package helm/peikarband --destination .
|
||||
helm repo index . --url https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}
|
||||
|
||||
- name: Upload Helm chart artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: helm-chart
|
||||
path: |
|
||||
*.tgz
|
||||
index.yaml
|
||||
|
||||
deploy-staging:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-and-push, package-helm]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
environment:
|
||||
name: staging
|
||||
url: https://staging.peikarband.ir
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
|
||||
- name: Configure kubectl
|
||||
run: |
|
||||
echo "${{ secrets.KUBECONFIG_STAGING }}" | base64 -d > kubeconfig
|
||||
export KUBECONFIG=kubeconfig
|
||||
|
||||
- name: Deploy to Staging
|
||||
run: |
|
||||
export KUBECONFIG=kubeconfig
|
||||
helm upgrade --install peikarband-staging ./helm/peikarband \
|
||||
--namespace staging \
|
||||
--create-namespace \
|
||||
--set image.tag=${{ needs.build-and-push.outputs.image-version }} \
|
||||
--set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
|
||||
--set ingress.hosts[0].host=staging.peikarband.ir \
|
||||
--wait \
|
||||
--timeout 5m
|
||||
|
||||
deploy-production:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-and-push, package-helm]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
environment:
|
||||
name: production
|
||||
url: https://peikarband.ir
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
|
||||
- name: Configure kubectl
|
||||
run: |
|
||||
echo "${{ secrets.KUBECONFIG_PRODUCTION }}" | base64 -d > kubeconfig
|
||||
export KUBECONFIG=kubeconfig
|
||||
|
||||
- name: Deploy to Production
|
||||
run: |
|
||||
export KUBECONFIG=kubeconfig
|
||||
helm upgrade --install peikarband-prod ./helm/peikarband \
|
||||
--namespace production \
|
||||
--create-namespace \
|
||||
--set image.tag=${{ needs.build-and-push.outputs.image-version }} \
|
||||
--set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
|
||||
--set replicaCount=3 \
|
||||
--set autoscaling.enabled=true \
|
||||
--values helm/peikarband/values-production.yaml \
|
||||
--wait \
|
||||
--timeout 10m
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
export KUBECONFIG=kubeconfig
|
||||
kubectl rollout status deployment/peikarband-prod -n production
|
||||
kubectl get pods -n production
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-and-push, package-helm, deploy-production]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download Helm chart
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: helm-chart
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
*.tgz
|
||||
index.yaml
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
121
.github/workflows/ci.yml
vendored
Normal file
121
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.11', '3.12']
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: peikarband_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache pip packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
flake8 src/ --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
flake8 src/ --count --max-complexity=10 --max-line-length=120 --statistics
|
||||
|
||||
- name: Type check with mypy
|
||||
run: |
|
||||
mypy src/
|
||||
|
||||
- name: Check formatting with black
|
||||
run: |
|
||||
black --check src/
|
||||
|
||||
- name: Check imports with isort
|
||||
run: |
|
||||
isort --check-only src/
|
||||
|
||||
- name: Run tests with pytest
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/peikarband_test
|
||||
REDIS_URL: redis://localhost:6379/0
|
||||
SECRET_KEY: test-secret-key
|
||||
JWT_SECRET_KEY: test-jwt-secret
|
||||
CELERY_BROKER_URL: redis://localhost:6379/1
|
||||
CELERY_RESULT_BACKEND: redis://localhost:6379/2
|
||||
run: |
|
||||
pytest tests/ -v --cov=src --cov-report=xml --cov-report=term-missing
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install bandit safety
|
||||
|
||||
- name: Run Bandit security scan
|
||||
run: |
|
||||
bandit -r src/ -f json -o bandit-report.json || true
|
||||
|
||||
- name: Run Safety check
|
||||
run: |
|
||||
safety check --json || true
|
||||
|
||||
66
.gitignore
vendored
Normal file
66
.gitignore
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Reflex
|
||||
.web/
|
||||
.reflex/
|
||||
.states/
|
||||
reflex.db
|
||||
reflex.db-*
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Temporary
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
|
||||
# Kubernetes secrets
|
||||
*secret*.yaml
|
||||
*Secret*.yaml
|
||||
43
.pre-commit-config.yaml
Normal file
43
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
# Pre-commit hooks for Peikarband
|
||||
# Install: pre-commit install
|
||||
# Run manually: pre-commit run --all-files
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-merge-conflict
|
||||
- id: debug-statements
|
||||
- id: mixed-line-ending
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.11
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black"]
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ["--max-line-length=120", "--extend-ignore=E203,W503"]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.7.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-all]
|
||||
args: ["--config-file=mypy.ini"]
|
||||
|
||||
71
Dockerfile
Normal file
71
Dockerfile
Normal file
@@ -0,0 +1,71 @@
|
||||
# Peikarband Platform - Production Dockerfile
|
||||
# Multi-stage build for optimized image size
|
||||
|
||||
# Stage 1: Builder
|
||||
FROM python:3.11-slim as builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
curl \
|
||||
gnupg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Node.js (required for Reflex)
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --user -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Initialize and build Reflex app
|
||||
RUN python -m reflex init --template blank && \
|
||||
python -m reflex export --frontend-only --no-zip
|
||||
|
||||
# Stage 2: Runtime
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
postgresql-client \
|
||||
curl \
|
||||
nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy Python dependencies from builder
|
||||
COPY --from=builder /root/.local /root/.local
|
||||
|
||||
# Copy application and built assets
|
||||
COPY --from=builder /build /app
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 peikarband && \
|
||||
chown -R peikarband:peikarband /app
|
||||
|
||||
# Set environment variables
|
||||
ENV PATH=/root/.local/bin:$PATH \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
REFLEX_ENV=production
|
||||
|
||||
USER peikarband
|
||||
|
||||
# Expose ports (backend: 8000, frontend: 3000)
|
||||
EXPOSE 3000 8000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/ping || exit 1
|
||||
|
||||
# Run application
|
||||
CMD ["python", "-m", "reflex", "run", "--env", "production", "--backend-only"]
|
||||
|
||||
126
Makefile
Normal file
126
Makefile
Normal file
@@ -0,0 +1,126 @@
|
||||
# Peikarband Platform - Makefile
|
||||
|
||||
REGISTRY ?= registry.example.com
|
||||
IMAGE_NAME ?= peikarband/landing
|
||||
VERSION ?= $(shell git describe --tags --always --dirty)
|
||||
HELM_RELEASE ?= peikarband
|
||||
NAMESPACE ?= production
|
||||
|
||||
.PHONY: help install dev test lint format clean docker-up docker-down migrate
|
||||
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo ""
|
||||
@echo "Development:"
|
||||
@echo " make install - Install dependencies"
|
||||
@echo " make dev - Run development server"
|
||||
@echo " make test - Run tests"
|
||||
@echo " make lint - Run linters"
|
||||
@echo " make format - Format code"
|
||||
@echo " make clean - Clean temporary files"
|
||||
@echo ""
|
||||
@echo "Docker:"
|
||||
@echo " make docker-build - Build Docker image"
|
||||
@echo " make docker-push - Push Docker image"
|
||||
@echo " make docker-up - Start Docker Compose"
|
||||
@echo " make docker-down - Stop Docker Compose"
|
||||
@echo ""
|
||||
@echo "Kubernetes/Helm:"
|
||||
@echo " make helm-lint - Lint Helm chart"
|
||||
@echo " make helm-package - Package Helm chart"
|
||||
@echo " make helm-install - Install Helm chart"
|
||||
@echo " make helm-upgrade - Upgrade Helm chart"
|
||||
@echo " make helm-uninstall - Uninstall Helm chart"
|
||||
@echo " make k8s-deploy - Deploy to Kubernetes"
|
||||
@echo ""
|
||||
@echo "Database:"
|
||||
@echo " make migrate - Run database migrations"
|
||||
|
||||
install:
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt
|
||||
pre-commit install
|
||||
|
||||
dev:
|
||||
python -m reflex run
|
||||
|
||||
test:
|
||||
pytest tests/ -v --cov=src --cov-report=html
|
||||
|
||||
lint:
|
||||
flake8 src/
|
||||
mypy src/
|
||||
black --check src/
|
||||
isort --check-only src/
|
||||
|
||||
format:
|
||||
black src/
|
||||
isort src/
|
||||
|
||||
clean:
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} +
|
||||
find . -type f -name "*.pyc" -delete
|
||||
find . -type f -name "*.pyo" -delete
|
||||
find . -type d -name ".pytest_cache" -exec rm -rf {} +
|
||||
find . -type d -name ".mypy_cache" -exec rm -rf {} +
|
||||
find . -type d -name "*.egg-info" -exec rm -rf {} +
|
||||
rm -rf .coverage htmlcov/
|
||||
rm -rf dist/ build/
|
||||
|
||||
# Docker commands
|
||||
docker-build:
|
||||
docker build -t $(IMAGE_NAME):$(VERSION) .
|
||||
docker tag $(IMAGE_NAME):$(VERSION) $(IMAGE_NAME):latest
|
||||
|
||||
docker-push:
|
||||
docker tag $(IMAGE_NAME):$(VERSION) $(REGISTRY)/$(IMAGE_NAME):$(VERSION)
|
||||
docker tag $(IMAGE_NAME):$(VERSION) $(REGISTRY)/$(IMAGE_NAME):latest
|
||||
docker push $(REGISTRY)/$(IMAGE_NAME):$(VERSION)
|
||||
docker push $(REGISTRY)/$(IMAGE_NAME):latest
|
||||
|
||||
docker-up:
|
||||
docker-compose up -d
|
||||
|
||||
docker-down:
|
||||
docker-compose down
|
||||
|
||||
# Helm commands
|
||||
helm-lint:
|
||||
helm lint helm/peikarband
|
||||
|
||||
helm-template:
|
||||
helm template $(HELM_RELEASE) helm/peikarband --debug
|
||||
|
||||
helm-package:
|
||||
helm package helm/peikarband --destination .
|
||||
|
||||
helm-install:
|
||||
helm install $(HELM_RELEASE) helm/peikarband \
|
||||
--namespace $(NAMESPACE) \
|
||||
--create-namespace \
|
||||
--set image.repository=$(REGISTRY)/$(IMAGE_NAME) \
|
||||
--set image.tag=$(VERSION) \
|
||||
--wait
|
||||
|
||||
helm-upgrade:
|
||||
helm upgrade --install $(HELM_RELEASE) helm/peikarband \
|
||||
--namespace $(NAMESPACE) \
|
||||
--set image.repository=$(REGISTRY)/$(IMAGE_NAME) \
|
||||
--set image.tag=$(VERSION) \
|
||||
--wait
|
||||
|
||||
helm-uninstall:
|
||||
helm uninstall $(HELM_RELEASE) --namespace $(NAMESPACE)
|
||||
|
||||
# Kubernetes deployment (full pipeline)
|
||||
k8s-deploy: docker-build docker-push helm-upgrade
|
||||
@echo "Deployment complete!"
|
||||
@echo "Check status: kubectl get pods -n $(NAMESPACE)"
|
||||
|
||||
# Database
|
||||
migrate:
|
||||
alembic upgrade head
|
||||
|
||||
seed:
|
||||
python scripts/seed_database.py
|
||||
|
||||
217
README.md
Normal file
217
README.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# پیکربند - پلتفرم جامع مدیریت هاستینگ و زیرساخت ابری
|
||||
|
||||
## 📖 درباره پروژه
|
||||
|
||||
پیکربند یک پلتفرم حرفهای برای مدیریت هاستینگ، سرورهای ابری، دامین و خدمات DevOps است. این پلتفرم با الهام از سرویسهایی مانند Cloudways، DigitalOcean و پارس پک طراحی شده است.
|
||||
|
||||
## 🏗️ معماری
|
||||
|
||||
این پروژه بر اساس **Clean Architecture** و اصول **SOLID** طراحی شده است:
|
||||
|
||||
- **Domain Layer**: منطق کسبوکار اصلی
|
||||
- **Application Layer**: موارد استفاده (Use Cases)
|
||||
- **Infrastructure Layer**: پیادهسازیهای فنی
|
||||
- **Presentation Layer**: رابط کاربری (Reflex)
|
||||
|
||||
## 🚀 تکنولوژیها
|
||||
|
||||
- **Frontend/Backend**: Python Reflex
|
||||
- **Database**: PostgreSQL + SQLAlchemy
|
||||
- **Cache**: Redis
|
||||
- **Task Queue**: Celery
|
||||
- **Testing**: pytest
|
||||
- **Code Quality**: black, flake8, mypy, isort
|
||||
|
||||
## 📋 پیشنیازها
|
||||
|
||||
- Python 3.11+
|
||||
- PostgreSQL 14+
|
||||
- Redis 7+
|
||||
- Node.js 18+ (برای Reflex)
|
||||
|
||||
## 🛠️ نصب و راهاندازی
|
||||
|
||||
### 1. کلون کردن پروژه
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/peikarband.git
|
||||
cd peikarband
|
||||
```
|
||||
|
||||
### 2. ایجاد محیط مجازی
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
```
|
||||
|
||||
### 3. نصب وابستگیها
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt # برای توسعه
|
||||
```
|
||||
|
||||
### 4. تنظیم Environment Variables
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# ویرایش .env و تکمیل مقادیر
|
||||
```
|
||||
|
||||
### 5. راهاندازی دیتابیس
|
||||
|
||||
```bash
|
||||
# ایجاد دیتابیس
|
||||
createdb peikarband
|
||||
|
||||
# اجرای migrations
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
### 6. اجرای پروژه
|
||||
|
||||
```bash
|
||||
# توسعه
|
||||
python -m reflex run
|
||||
|
||||
# یا
|
||||
make dev
|
||||
```
|
||||
|
||||
## 🚢 Deployment
|
||||
|
||||
### با Docker
|
||||
|
||||
```bash
|
||||
# Build
|
||||
docker build -t peikarband:latest .
|
||||
|
||||
# Run
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### با Kubernetes/Helm
|
||||
|
||||
```bash
|
||||
# Deploy
|
||||
helm upgrade --install peikarband ./helm/peikarband \
|
||||
--namespace production \
|
||||
--set image.tag=0.1.0
|
||||
|
||||
# یا
|
||||
make k8s-deploy
|
||||
```
|
||||
|
||||
📖 [راهنمای کامل Deployment](docs/deployment/kubernetes.md)
|
||||
|
||||
## 📁 ساختار پروژه
|
||||
|
||||
```
|
||||
peikarband/
|
||||
├── docs/ # مستندات
|
||||
├── src/
|
||||
│ ├── config/ # تنظیمات
|
||||
│ ├── core/ # هسته اصلی
|
||||
│ │ ├── domain/ # Domain entities & logic
|
||||
│ │ └── application/ # Use cases & DTOs
|
||||
│ ├── infrastructure/ # پیادهسازیهای فنی
|
||||
│ ├── presentation/ # رابط کاربری
|
||||
│ └── shared/ # کدهای مشترک
|
||||
├── tests/ # تستها
|
||||
└── scripts/ # اسکریپتهای کمکی
|
||||
```
|
||||
|
||||
## 🧪 تست
|
||||
|
||||
```bash
|
||||
# اجرای همه تستها
|
||||
pytest
|
||||
|
||||
# با coverage
|
||||
pytest --cov=src tests/
|
||||
|
||||
# تستهای خاص
|
||||
pytest tests/unit/
|
||||
pytest tests/integration/
|
||||
```
|
||||
|
||||
## 📝 کدنویسی
|
||||
|
||||
### استانداردها
|
||||
|
||||
- **PEP 8**: استاندارد کدنویسی Python
|
||||
- **PEP 20**: Zen of Python
|
||||
- **Type Hints**: همه جا استفاده شود
|
||||
- **Docstrings**: Google Style
|
||||
|
||||
### ابزارهای کیفیت کد
|
||||
|
||||
```bash
|
||||
# Format
|
||||
black src/
|
||||
|
||||
# Linting
|
||||
flake8 src/
|
||||
|
||||
# Type checking
|
||||
mypy src/
|
||||
|
||||
# Import sorting
|
||||
isort src/
|
||||
```
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
## 📚 مستندات
|
||||
|
||||
مستندات کامل در پوشه `docs/` موجود است:
|
||||
|
||||
- [Handbook](docs/handbook.md): راهنمای جامع پروژه
|
||||
- [Architecture](docs/architecture/): معماری سیستم
|
||||
- [Development](docs/development/): راهنمای توسعه
|
||||
- [API Reference](docs/api/): مستندات API
|
||||
|
||||
## 🔐 امنیت
|
||||
|
||||
- همه پسوردها با bcrypt hash میشوند
|
||||
- استفاده از JWT برای authentication
|
||||
- پشتیبانی از 2FA
|
||||
- اطلاعات حساس رمزنگاری میشوند
|
||||
|
||||
## 🤝 مشارکت
|
||||
|
||||
برای مشارکت در پروژه:
|
||||
|
||||
1. Fork کنید
|
||||
2. Branch جدید بسازید (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit کنید (`git commit -m 'feat: add amazing feature'`)
|
||||
4. Push کنید (`git push origin feature/amazing-feature`)
|
||||
5. Pull Request بسازید
|
||||
|
||||
## 📄 لایسنس
|
||||
|
||||
این پروژه تحت لایسنس MIT منتشر شده است.
|
||||
|
||||
## 👥 تیم
|
||||
|
||||
- Lead Developer: [Your Name]
|
||||
- Architecture: Clean Architecture
|
||||
- Methodology: Agile/Scrum
|
||||
|
||||
## 📞 تماس
|
||||
|
||||
- Website: https://peikarband.ir
|
||||
- Email: support@peikarband.ir
|
||||
- Telegram: @peikarband
|
||||
|
||||
---
|
||||
|
||||
**نسخه**: 0.1.0
|
||||
**آخرین بروزرسانی**: 2025-01-24
|
||||
|
||||
102
alembic.ini
Normal file
102
alembic.ini
Normal file
@@ -0,0 +1,102 @@
|
||||
# Alembic configuration file
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = src/infrastructure/database/migrations
|
||||
|
||||
# template used to generate migration files
|
||||
file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to migrations/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = postgresql://username:password@localhost/peikarband
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
|
||||
154
argocd/README.md
Normal file
154
argocd/README.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# ArgoCD Deployment
|
||||
|
||||
This directory contains ArgoCD Application manifests for deploying Peikarband to Kubernetes.
|
||||
|
||||
## Files
|
||||
|
||||
- `application.yaml`: Production deployment (main branch → peikarband namespace)
|
||||
- `application-staging.yaml`: Staging deployment (develop branch → peikarband-staging namespace)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. ArgoCD installed in your cluster
|
||||
2. Git repository access configured in ArgoCD
|
||||
3. Docker registry credentials (if using private registry)
|
||||
|
||||
## Deployment
|
||||
|
||||
### 1. Add Git Repository to ArgoCD
|
||||
|
||||
```bash
|
||||
# For SSH
|
||||
argocd repo add git@git.peikarband.ir:ehsan-minadd/peikarband.git \
|
||||
--ssh-private-key-path ~/.ssh/id_rsa \
|
||||
--insecure-skip-server-verification
|
||||
|
||||
# Or using argocd UI: Settings → Repositories → Connect Repo
|
||||
```
|
||||
|
||||
### 2. Deploy Production
|
||||
|
||||
```bash
|
||||
kubectl apply -f argocd/application.yaml
|
||||
```
|
||||
|
||||
### 3. Deploy Staging
|
||||
|
||||
```bash
|
||||
kubectl apply -f argocd/application-staging.yaml
|
||||
```
|
||||
|
||||
## Sync Policy
|
||||
|
||||
Both applications use **automatic sync** with:
|
||||
- **Auto-prune**: Remove resources deleted from Git
|
||||
- **Self-heal**: Automatically sync when cluster state differs from Git
|
||||
- **Retry logic**: 5 attempts with exponential backoff
|
||||
|
||||
## Monitoring
|
||||
|
||||
```bash
|
||||
# Check application status
|
||||
argocd app get peikarband
|
||||
argocd app get peikarband-staging
|
||||
|
||||
# Watch sync progress
|
||||
argocd app sync peikarband --watch
|
||||
|
||||
# View logs
|
||||
argocd app logs peikarband
|
||||
```
|
||||
|
||||
## Manual Sync
|
||||
|
||||
```bash
|
||||
# Force sync
|
||||
argocd app sync peikarband --force
|
||||
|
||||
# Sync with prune
|
||||
argocd app sync peikarband --prune
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
```bash
|
||||
# List history
|
||||
argocd app history peikarband
|
||||
|
||||
# Rollback to specific revision
|
||||
argocd app rollback peikarband <REVISION>
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ ArgoCD │
|
||||
│ ┌───────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Production App │ │ Staging App │ │
|
||||
│ │ (main branch) │ │ (develop branch) │ │
|
||||
│ └─────────┬─────────┘ └────────┬─────────┘ │
|
||||
└────────────┼─────────────────────┼──────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────────────┐ ┌─────────────────┐
|
||||
│ namespace: │ │ namespace: │
|
||||
│ peikarband │ │ peikarband-stg │
|
||||
└────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Override via Helm values:
|
||||
|
||||
```yaml
|
||||
# In values-production.yaml or values-staging.yaml
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
value: "postgresql://..."
|
||||
- name: REDIS_URL
|
||||
value: "redis://..."
|
||||
```
|
||||
|
||||
## Secrets Management
|
||||
|
||||
Secrets should be managed outside Git:
|
||||
|
||||
```bash
|
||||
# Using kubectl
|
||||
kubectl create secret generic peikarband-secrets \
|
||||
--from-literal=database-password=xxx \
|
||||
--namespace=peikarband
|
||||
|
||||
# Or using Sealed Secrets, External Secrets Operator, etc.
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Application Out of Sync
|
||||
|
||||
```bash
|
||||
argocd app sync peikarband --force
|
||||
```
|
||||
|
||||
### Image Pull Errors
|
||||
|
||||
Check registry credentials:
|
||||
```bash
|
||||
kubectl get secret regcred -n peikarband -o yaml
|
||||
```
|
||||
|
||||
### Health Check Failing
|
||||
|
||||
View pod logs:
|
||||
```bash
|
||||
kubectl logs -n peikarband -l app=peikarband --tail=100
|
||||
```
|
||||
|
||||
### Helm Values Override Not Working
|
||||
|
||||
Verify values file path in Application manifest:
|
||||
```bash
|
||||
argocd app manifests peikarband | grep valueFiles
|
||||
```
|
||||
|
||||
47
argocd/application-staging.yaml
Normal file
47
argocd/application-staging.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: peikarband-staging
|
||||
namespace: argocd
|
||||
finalizers:
|
||||
- resources-finalizer.argocd.argoproj.io
|
||||
spec:
|
||||
project: default
|
||||
|
||||
source:
|
||||
repoURL: git@git.peikarband.ir:ehsan-minadd/peikarband.git
|
||||
targetRevision: develop
|
||||
path: helm/peikarband
|
||||
helm:
|
||||
releaseName: peikarband-staging
|
||||
valueFiles:
|
||||
- values-staging.yaml
|
||||
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: peikarband-staging
|
||||
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
allowEmpty: false
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
- PrunePropagationPolicy=foreground
|
||||
- PruneLast=true
|
||||
retry:
|
||||
limit: 5
|
||||
backoff:
|
||||
duration: 5s
|
||||
factor: 2
|
||||
maxDuration: 3m
|
||||
|
||||
revisionHistoryLimit: 10
|
||||
|
||||
ignoreDifferences:
|
||||
- group: apps
|
||||
kind: Deployment
|
||||
jsonPointers:
|
||||
- /spec/replicas
|
||||
|
||||
47
argocd/application.yaml
Normal file
47
argocd/application.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: peikarband
|
||||
namespace: argocd
|
||||
finalizers:
|
||||
- resources-finalizer.argocd.argoproj.io
|
||||
spec:
|
||||
project: default
|
||||
|
||||
source:
|
||||
repoURL: git@git.peikarband.ir:ehsan-minadd/peikarband.git
|
||||
targetRevision: HEAD
|
||||
path: helm/peikarband
|
||||
helm:
|
||||
releaseName: peikarband
|
||||
valueFiles:
|
||||
- values-production.yaml
|
||||
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: peikarband
|
||||
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
allowEmpty: false
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
- PrunePropagationPolicy=foreground
|
||||
- PruneLast=true
|
||||
retry:
|
||||
limit: 5
|
||||
backoff:
|
||||
duration: 5s
|
||||
factor: 2
|
||||
maxDuration: 3m
|
||||
|
||||
revisionHistoryLimit: 10
|
||||
|
||||
ignoreDifferences:
|
||||
- group: apps
|
||||
kind: Deployment
|
||||
jsonPointers:
|
||||
- /spec/replicas
|
||||
|
||||
31
assets/custom.css
Normal file
31
assets/custom.css
Normal file
@@ -0,0 +1,31 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100..900&display=swap');
|
||||
|
||||
body {
|
||||
font-family: 'Vazirmatn', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
0% { box-shadow: 0 12px 40px rgba(30, 64, 175, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.2); }
|
||||
50% { box-shadow: 0 18px 50px rgba(30, 64, 175, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.3); }
|
||||
100% { box-shadow: 0 12px 40px rgba(30, 64, 175, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.2); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.05); opacity: 0.8; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-20px); }
|
||||
}
|
||||
|
||||
92
docker-compose.yml
Normal file
92
docker-compose.yml
Normal file
@@ -0,0 +1,92 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
container_name: peikarband-db
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-peikarband}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-peikarband}
|
||||
POSTGRES_DB: ${DB_NAME:-peikarband}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U peikarband"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Redis Cache
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: peikarband-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Peikarband Application
|
||||
app:
|
||||
build: .
|
||||
container_name: peikarband-app
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://peikarband:peikarband@postgres:5432/peikarband
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- CELERY_BROKER_URL=redis://redis:6379/1
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/2
|
||||
- SECRET_KEY=${SECRET_KEY}
|
||||
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
|
||||
- ENVIRONMENT=production
|
||||
volumes:
|
||||
- ./:/app
|
||||
restart: unless-stopped
|
||||
|
||||
# Celery Worker
|
||||
celery:
|
||||
build: .
|
||||
container_name: peikarband-celery
|
||||
command: celery -A src.infrastructure.tasks.celery_app worker -l info
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://peikarband:peikarband@postgres:5432/peikarband
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- CELERY_BROKER_URL=redis://redis:6379/1
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/2
|
||||
volumes:
|
||||
- ./:/app
|
||||
restart: unless-stopped
|
||||
|
||||
# Flower (Celery Monitoring)
|
||||
flower:
|
||||
build: .
|
||||
container_name: peikarband-flower
|
||||
command: celery -A src.infrastructure.tasks.celery_app flower
|
||||
depends_on:
|
||||
- redis
|
||||
- celery
|
||||
ports:
|
||||
- "5555:5555"
|
||||
environment:
|
||||
- CELERY_BROKER_URL=redis://redis:6379/1
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
|
||||
453
docs/architecture/database-strategy.md
Normal file
453
docs/architecture/database-strategy.md
Normal file
@@ -0,0 +1,453 @@
|
||||
# استراتژی دیتابیس - انعطاف و مقیاسپذیری
|
||||
|
||||
## مشکل: انعطاف vs ثبات
|
||||
|
||||
با توجه به اینکه پروژه قرار است:
|
||||
- کلی feature جدید بگیره
|
||||
- Options و configurations متنوع داشته باشه
|
||||
- قابل توسعه سریع باشه
|
||||
|
||||
آیا PostgreSQL محدودیت ایجاد میکنه؟
|
||||
|
||||
## ✅ راه حل: Hybrid Database Architecture
|
||||
|
||||
### لایهبندی داده بر اساس نوع:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
└────┬─────────────┬──────────────┬────────────┘
|
||||
│ │ │
|
||||
┌────▼─────┐ ┌───▼────┐ ┌───▼────┐
|
||||
│PostgreSQL│ │MongoDB │ │ Redis │
|
||||
│ │ │ │ │ │
|
||||
│Critical │ │Flexible│ │ Cache │
|
||||
│Structured│ │Dynamic │ │Session │
|
||||
└──────────┘ └────────┘ └────────┘
|
||||
```
|
||||
|
||||
### Tier 1: PostgreSQL - Critical Business Data
|
||||
|
||||
**چی بریزیم توی PostgreSQL:**
|
||||
- User accounts
|
||||
- Financial data (invoices, transactions, wallet)
|
||||
- Service subscriptions
|
||||
- Audit logs
|
||||
- Anything که نیاز به ACID داره
|
||||
|
||||
```python
|
||||
# ❌ اینها رو NEVER توی MongoDB نذاریم
|
||||
class Invoice(BaseModel):
|
||||
id: int
|
||||
user_id: int
|
||||
amount: Decimal # MUST be ACID
|
||||
status: str
|
||||
paid_at: datetime
|
||||
```
|
||||
|
||||
**چرا PostgreSQL؟**
|
||||
- ✅ ACID Transactions
|
||||
- ✅ Data Integrity
|
||||
- ✅ Complex Joins
|
||||
- ✅ Proven for financial data
|
||||
|
||||
### Tier 2: PostgreSQL JSONB - Flexible Structured
|
||||
|
||||
**چی بریزیم توی JSONB:**
|
||||
- Service configurations
|
||||
- User preferences
|
||||
- Feature flags
|
||||
- Custom metadata
|
||||
- Plugin settings
|
||||
|
||||
```python
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
class Service(BaseModel):
|
||||
# Structured
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer)
|
||||
type = Column(String) # 'vps', 'hosting', etc.
|
||||
|
||||
# Flexible - هر چیزی میتونه باشه!
|
||||
config = Column(JSONB, default={})
|
||||
metadata = Column(JSONB, default={})
|
||||
|
||||
# Example 1: VPS
|
||||
vps = Service(
|
||||
type="vps",
|
||||
config={
|
||||
"cpu": 4,
|
||||
"ram": 8,
|
||||
"disk": 100,
|
||||
"os": "ubuntu-22.04"
|
||||
}
|
||||
)
|
||||
|
||||
# Example 2: WordPress Hosting
|
||||
wp = Service(
|
||||
type="wordpress",
|
||||
config={
|
||||
"domain": "example.com",
|
||||
"php_version": "8.2",
|
||||
"auto_update": True,
|
||||
"cdn_enabled": True,
|
||||
# Future features - بدون migration!
|
||||
"new_feature_2025": {"enabled": True}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**Query کردن JSONB:**
|
||||
|
||||
```python
|
||||
# پیدا کردن VPS های بالای 4 CPU
|
||||
services = session.query(Service).filter(
|
||||
Service.config['cpu'].astext.cast(Integer) >= 4
|
||||
).all()
|
||||
|
||||
# پیدا کردن WordPress با CDN enabled
|
||||
services = session.query(Service).filter(
|
||||
Service.config['cdn_enabled'].astext == 'true'
|
||||
).all()
|
||||
|
||||
# Index روی JSONB برای performance
|
||||
from sqlalchemy import Index
|
||||
|
||||
Index(
|
||||
'idx_service_config_cpu',
|
||||
Service.config['cpu'].astext.cast(Integer)
|
||||
)
|
||||
```
|
||||
|
||||
### Tier 3: MongoDB - Completely Dynamic
|
||||
|
||||
**چی بریزیم توی MongoDB:**
|
||||
- Logs و events
|
||||
- Analytics data
|
||||
- User activity tracking
|
||||
- System metrics
|
||||
- Unstructured data
|
||||
|
||||
```python
|
||||
# MongoDB - No schema!
|
||||
{
|
||||
"_id": "service_log_123",
|
||||
"service_id": 123,
|
||||
"timestamp": "2025-01-24T...",
|
||||
"events": [
|
||||
{
|
||||
"type": "cpu_spike",
|
||||
"value": 95,
|
||||
"custom_field_1": "...",
|
||||
# هر چیزی که بخوایم!
|
||||
}
|
||||
],
|
||||
"metrics": {
|
||||
# Structure آزاد
|
||||
"anything": "goes here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## پیادهسازی در کد
|
||||
|
||||
### 1. Service با JSONB:
|
||||
|
||||
```python
|
||||
# src/infrastructure/database/models/service_model.py
|
||||
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Enum as SQLEnum
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import relationship
|
||||
import enum
|
||||
|
||||
class ServiceType(enum.Enum):
|
||||
VPS = "vps"
|
||||
HOSTING = "hosting"
|
||||
WORDPRESS = "wordpress"
|
||||
DOMAIN = "domain"
|
||||
|
||||
class ServiceModel(BaseModel):
|
||||
__tablename__ = "services"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
type = Column(SQLEnum(ServiceType), nullable=False)
|
||||
status = Column(String(20), nullable=False)
|
||||
|
||||
# Flexible configuration
|
||||
config = Column(JSONB, nullable=False, default={})
|
||||
metadata = Column(JSONB, nullable=False, default={})
|
||||
|
||||
# Relationships
|
||||
user = relationship("UserModel", back_populates="services")
|
||||
```
|
||||
|
||||
### 2. Repository با JSONB support:
|
||||
|
||||
```python
|
||||
# src/infrastructure/database/repositories/service_repository.py
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
class ServiceRepository:
|
||||
def __init__(self, session: Session):
|
||||
self._session = session
|
||||
|
||||
def find_by_config(
|
||||
self,
|
||||
json_path: str,
|
||||
value: Any
|
||||
) -> List[ServiceModel]:
|
||||
"""Query by JSONB field.
|
||||
|
||||
Example:
|
||||
repo.find_by_config('cpu', 4)
|
||||
repo.find_by_config('cdn_enabled', True)
|
||||
"""
|
||||
return self._session.query(ServiceModel).filter(
|
||||
ServiceModel.config[json_path].astext == str(value)
|
||||
).all()
|
||||
|
||||
def update_config(
|
||||
self,
|
||||
service_id: int,
|
||||
config_updates: Dict[str, Any]
|
||||
) -> ServiceModel:
|
||||
"""Update service config partially."""
|
||||
service = self.get_by_id(service_id)
|
||||
if service:
|
||||
# Merge configs
|
||||
new_config = {**service.config, **config_updates}
|
||||
service.config = new_config
|
||||
self._session.commit()
|
||||
return service
|
||||
```
|
||||
|
||||
### 3. Domain Entity که flexible هست:
|
||||
|
||||
```python
|
||||
# src/core/domain/entities/service.py
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from src.core.domain.entities.base import BaseEntity
|
||||
|
||||
class Service(BaseEntity):
|
||||
"""Service domain entity with flexible configuration."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: Optional[int] = None,
|
||||
user_id: int = None,
|
||||
type: str = None,
|
||||
status: str = None,
|
||||
config: Dict[str, Any] = None,
|
||||
metadata: Dict[str, Any] = None,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(id=id, **kwargs)
|
||||
self.user_id = user_id
|
||||
self.type = type
|
||||
self.status = status
|
||||
self.config = config or {}
|
||||
self.metadata = metadata or {}
|
||||
|
||||
def get_config(self, key: str, default: Any = None) -> Any:
|
||||
"""Get config value safely."""
|
||||
return self.config.get(key, default)
|
||||
|
||||
def set_config(self, key: str, value: Any) -> None:
|
||||
"""Set config value."""
|
||||
self.config[key] = value
|
||||
|
||||
def update_config(self, updates: Dict[str, Any]) -> None:
|
||||
"""Update multiple config values."""
|
||||
self.config.update(updates)
|
||||
```
|
||||
|
||||
## مقایسه روشها
|
||||
|
||||
### Migration-based (Traditional SQL)
|
||||
|
||||
```python
|
||||
# ❌ هر feature جدید = migration جدید
|
||||
|
||||
# Migration 001
|
||||
ALTER TABLE services ADD COLUMN cpu INTEGER;
|
||||
|
||||
# Migration 002
|
||||
ALTER TABLE services ADD COLUMN ram INTEGER;
|
||||
|
||||
# Migration 003
|
||||
ALTER TABLE services ADD COLUMN new_feature VARCHAR(255);
|
||||
|
||||
# بعد از 100 feature = 100 migration! 😱
|
||||
```
|
||||
|
||||
### JSONB-based (Flexible SQL)
|
||||
|
||||
```python
|
||||
# ✅ هیچ migration لازم نیست!
|
||||
|
||||
# روز اول
|
||||
service.config = {"cpu": 4}
|
||||
|
||||
# یک ماه بعد
|
||||
service.config = {"cpu": 4, "ram": 8}
|
||||
|
||||
# یک سال بعد
|
||||
service.config = {
|
||||
"cpu": 4,
|
||||
"ram": 8,
|
||||
"new_feature_2025": True,
|
||||
"another_feature": {"nested": "data"}
|
||||
}
|
||||
|
||||
# بدون هیچ migration! 🎉
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. تصمیمگیری: PostgreSQL vs MongoDB vs JSONB
|
||||
|
||||
```python
|
||||
# PostgreSQL (Structured)
|
||||
✅ User authentication
|
||||
✅ Financial transactions
|
||||
✅ Invoices
|
||||
✅ Core business entities
|
||||
|
||||
# PostgreSQL JSONB
|
||||
✅ Service configurations
|
||||
✅ User preferences
|
||||
✅ Feature flags
|
||||
✅ Plugin settings
|
||||
✅ Custom fields
|
||||
|
||||
# MongoDB
|
||||
✅ Logs & Events
|
||||
✅ Analytics
|
||||
✅ User activity
|
||||
✅ System metrics
|
||||
✅ Temporary data
|
||||
```
|
||||
|
||||
### 2. JSONB Schema Validation (در Application)
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class VPSConfig(BaseModel):
|
||||
"""Schema for VPS service config."""
|
||||
cpu: int
|
||||
ram: int
|
||||
disk: int
|
||||
os: str
|
||||
backups: Optional[bool] = False
|
||||
|
||||
class WordPressConfig(BaseModel):
|
||||
"""Schema for WordPress service config."""
|
||||
domain: str
|
||||
php_version: str
|
||||
auto_update: bool
|
||||
cdn_enabled: Optional[bool] = False
|
||||
|
||||
# Validation
|
||||
def validate_service_config(service_type: str, config: dict):
|
||||
"""Validate config based on service type."""
|
||||
schemas = {
|
||||
"vps": VPSConfig,
|
||||
"wordpress": WordPressConfig,
|
||||
}
|
||||
schema = schemas.get(service_type)
|
||||
if schema:
|
||||
return schema(**config) # Validates
|
||||
return config
|
||||
```
|
||||
|
||||
### 3. Indexing برای Performance
|
||||
|
||||
```python
|
||||
# در migration
|
||||
from alembic import op
|
||||
|
||||
def upgrade():
|
||||
# Create GIN index on JSONB
|
||||
op.execute("""
|
||||
CREATE INDEX idx_services_config_gin
|
||||
ON services USING GIN (config);
|
||||
""")
|
||||
|
||||
# Index specific keys
|
||||
op.execute("""
|
||||
CREATE INDEX idx_services_config_cpu
|
||||
ON services ((config->>'cpu'));
|
||||
""")
|
||||
```
|
||||
|
||||
## مزایا و معایب
|
||||
|
||||
### PostgreSQL + JSONB (پیشنهاد من)
|
||||
|
||||
**مزایا:**
|
||||
- ✅ انعطاف بالا (مثل MongoDB)
|
||||
- ✅ ACID transactions (برای billing امن)
|
||||
- ✅ Query قدرتمند
|
||||
- ✅ یک database کمتر = سادهتر
|
||||
- ✅ Performance خوب با indexing
|
||||
- ✅ Data integrity
|
||||
|
||||
**معایب:**
|
||||
- ❌ JSONB query ها کمی پیچیدهتر از SQL معمولی
|
||||
- ❌ نیاز به validation در application layer
|
||||
|
||||
### Hybrid (PostgreSQL + MongoDB)
|
||||
|
||||
**مزایا:**
|
||||
- ✅ Best of both worlds
|
||||
- ✅ Separation of concerns
|
||||
- ✅ Optimal performance
|
||||
|
||||
**معایب:**
|
||||
- ❌ پیچیدگی بیشتر
|
||||
- ❌ دو database = maintenance بیشتر
|
||||
- ❌ Consistency بین دو DB
|
||||
|
||||
## نتیجهگیری
|
||||
|
||||
**پیشنهاد برای پیکربند:**
|
||||
|
||||
```python
|
||||
"""
|
||||
Tier 1: PostgreSQL - Critical
|
||||
- Users, Auth
|
||||
- Invoices, Transactions
|
||||
- Wallet, Payments
|
||||
|
||||
Tier 2: PostgreSQL JSONB - Flexible
|
||||
- Service configs
|
||||
- User preferences
|
||||
- Custom settings
|
||||
|
||||
Tier 3: Redis - Cache
|
||||
- Sessions
|
||||
- Cache
|
||||
- Rate limiting
|
||||
|
||||
(Optional) Tier 4: MongoDB - Logs
|
||||
- Activity logs
|
||||
- System metrics
|
||||
- Analytics
|
||||
"""
|
||||
```
|
||||
|
||||
**در عمل:**
|
||||
- شروع با PostgreSQL + JSONB
|
||||
- اگر لازم شد، MongoDB اضافه میکنیم
|
||||
- ساده، flexible، و قابل توسعه
|
||||
|
||||
**PostgreSQL دست و پای ما رو نمیبنده، اگر از JSONB استفاده کنیم!** ✨
|
||||
|
||||
174
docs/architecture/overview.md
Normal file
174
docs/architecture/overview.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# معماری کلی سیستم
|
||||
|
||||
## نمای کلی
|
||||
|
||||
پلتفرم پیکربند بر اساس **Clean Architecture** طراحی شده که قابلیت تست، نگهداری و توسعه را به حداکثر میرساند.
|
||||
|
||||
## لایههای معماری
|
||||
|
||||
### 1. Domain Layer (هسته مرکزی)
|
||||
|
||||
مستقلترین لایه که شامل منطق کسبوکار خالص است:
|
||||
|
||||
**Components:**
|
||||
- **Entities**: موجودیتهای اصلی (User, Service, Invoice, Server)
|
||||
- **Value Objects**: Email, Money, Phone, IPAddress
|
||||
- **Domain Services**: منطق پیچیدهای که به چند entity مرتبط است
|
||||
- **Domain Events**: رویدادهای کسبوکار
|
||||
- **Exceptions**: خطاهای دامین
|
||||
|
||||
**قوانین:**
|
||||
- هیچ وابستگی به لایههای دیگر ندارد
|
||||
- فقط منطق کسبوکار
|
||||
- بدون وابستگی به framework
|
||||
- Pure Python
|
||||
|
||||
### 2. Application Layer (موارد استفاده)
|
||||
|
||||
**Components:**
|
||||
- **Use Cases**: موارد استفاده سیستم (RegisterUser, CreateInvoice)
|
||||
- **DTOs**: Data Transfer Objects
|
||||
- **Interfaces**: تعریف رابطهای سرویسها
|
||||
- **Validators**: اعتبارسنجی ورودیها
|
||||
|
||||
**قوانین:**
|
||||
- وابسته به Domain Layer
|
||||
- مستقل از Infrastructure
|
||||
- تعریف رابطهای مورد نیاز
|
||||
|
||||
### 3. Infrastructure Layer (جزئیات فنی)
|
||||
|
||||
**Components:**
|
||||
- **Database**: PostgreSQL + SQLAlchemy
|
||||
- **Cache**: Redis
|
||||
- **External APIs**: DigitalOcean, Hetzner, OVH
|
||||
- **Tasks**: Celery background jobs
|
||||
- **Security**: Authentication, Authorization
|
||||
- **Logging**: Structured logging
|
||||
|
||||
**قوانین:**
|
||||
- پیادهسازی interface های Application Layer
|
||||
- وابسته به تکنولوژیهای خاص
|
||||
- قابل تعویض
|
||||
|
||||
### 4. Presentation Layer (رابط کاربری)
|
||||
|
||||
**Components:**
|
||||
- **Web**: Reflex pages و components
|
||||
- **API**: REST endpoints (optional)
|
||||
- **State Management**: Reflex states
|
||||
|
||||
**قوانین:**
|
||||
- فقط به Application Layer وابسته
|
||||
- مستقل از Infrastructure details
|
||||
|
||||
## جریان داده
|
||||
|
||||
```
|
||||
User Action
|
||||
↓
|
||||
Presentation Layer (Reflex Component)
|
||||
↓
|
||||
Application Layer (Use Case)
|
||||
↓
|
||||
Domain Layer (Business Logic)
|
||||
↓
|
||||
Application Layer (Interfaces)
|
||||
↓
|
||||
Infrastructure Layer (Implementation)
|
||||
↓
|
||||
External Systems (Database, APIs)
|
||||
```
|
||||
|
||||
## Dependency Rule
|
||||
|
||||
وابستگیها همیشه به سمت داخل (به سمت Domain) هستند:
|
||||
|
||||
```
|
||||
Presentation → Application → Domain
|
||||
Infrastructure → Application → Domain
|
||||
```
|
||||
|
||||
**قانون طلایی**: لایههای داخلی هیچ چیز از لایههای خارجی نمیدانند.
|
||||
|
||||
## مزایای این معماری
|
||||
|
||||
1. **Testability**: هر لایه مستقلا قابل تست
|
||||
2. **Maintainability**: تغییرات محلی و جداسازی شده
|
||||
3. **Flexibility**: تعویض آسان تکنولوژیها
|
||||
4. **Scalability**: قابل مقیاسپذیری در هر لایه
|
||||
5. **Business Logic First**: تمرکز روی منطق کسبوکار
|
||||
|
||||
## Domain-Driven Design (DDD)
|
||||
|
||||
پروژه از اصول DDD استفاده میکند:
|
||||
|
||||
- **Ubiquitous Language**: زبان مشترک با کسبوکار
|
||||
- **Bounded Contexts**: محدودههای مشخص
|
||||
- **Aggregates**: مجموعههای یکپارچه
|
||||
- **Repositories**: دسترسی به داده
|
||||
- **Domain Events**: رویدادهای کسبوکار
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Core
|
||||
- **Language**: Python 3.11+
|
||||
- **Framework**: Reflex 0.4.0
|
||||
- **Database**: PostgreSQL 14+
|
||||
- **Cache**: Redis 7+
|
||||
- **ORM**: SQLAlchemy 2.0+
|
||||
|
||||
### Infrastructure
|
||||
- **Task Queue**: Celery 5.3+
|
||||
- **Testing**: pytest 7.4+
|
||||
- **Logging**: structlog
|
||||
- **API Client**: httpx
|
||||
|
||||
### External Services
|
||||
- **Cloud Providers**: DigitalOcean, Hetzner, OVH
|
||||
- **Payment**: Zarinpal, IDPay
|
||||
- **Monitoring**: Sentry, Prometheus
|
||||
|
||||
## Security Architecture
|
||||
|
||||
- **Authentication**: JWT + 2FA
|
||||
- **Authorization**: RBAC (Role-Based Access Control)
|
||||
- **Encryption**: Data at rest & in transit
|
||||
- **Secrets Management**: Environment variables
|
||||
- **Audit Logging**: تمام اقدامات مهم
|
||||
|
||||
## Scalability Strategy
|
||||
|
||||
### Horizontal Scaling
|
||||
- Load balancing برای Reflex
|
||||
- Database replication
|
||||
- Redis clustering
|
||||
- Celery workers
|
||||
|
||||
### Vertical Scaling
|
||||
- Connection pooling
|
||||
- Query optimization
|
||||
- Caching strategy
|
||||
- Async operations
|
||||
|
||||
## Monitoring & Observability
|
||||
|
||||
- **Metrics**: Request rate, response time, error rate
|
||||
- **Logs**: Structured logging با contextual info
|
||||
- **Tracing**: Request tracing
|
||||
- **Alerts**: Critical issues
|
||||
- **Dashboards**: Grafana/Prometheus
|
||||
|
||||
## Future Considerations
|
||||
|
||||
- Microservices architecture (در صورت نیاز)
|
||||
- Event-driven architecture
|
||||
- CQRS pattern
|
||||
- GraphQL API
|
||||
- Multi-tenancy
|
||||
|
||||
---
|
||||
|
||||
**نسخه**: 1.0.0
|
||||
**آخرین بروزرسانی**: 2025-01-24
|
||||
|
||||
42
docs/changelog/CHANGELOG.md
Normal file
42
docs/changelog/CHANGELOG.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
تمام تغییرات قابل توجه این پروژه در این فایل مستند میشود.
|
||||
|
||||
فرمت بر اساس [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) است،
|
||||
و این پروژه از [Semantic Versioning](https://semver.org/spec/v2.0.0.html) پیروی میکند.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- معماری اولیه پروژه بر اساس Clean Architecture
|
||||
- ساختار پوشههای اصلی
|
||||
- تنظیمات config با Pydantic
|
||||
- Setup PostgreSQL و SQLAlchemy
|
||||
- Setup Redis برای cache
|
||||
- Structured logging با structlog
|
||||
- pytest برای testing
|
||||
- Pre-commit hooks
|
||||
- مستندات جامع (Handbook)
|
||||
|
||||
### Changed
|
||||
- انتقال landing page به ساختار جدید
|
||||
|
||||
## [0.1.0] - 2025-01-24
|
||||
|
||||
### Added
|
||||
- شروع پروژه
|
||||
- Landing page اولیه با Reflex
|
||||
- رنگبندی آبی مطابق لوگو
|
||||
- بخش ویژه WordPress Cloud
|
||||
- Badge "بزودی کنار شما خواهیم بود"
|
||||
- فونت فارسی Vazirmatn
|
||||
|
||||
### Design
|
||||
- Hero section با gradient animations
|
||||
- Service cards
|
||||
- Pricing section
|
||||
- Server comparison table
|
||||
- Testimonials
|
||||
- Contact form
|
||||
- Floating chat button
|
||||
|
||||
69
docs/changelog/known-issues.md
Normal file
69
docs/changelog/known-issues.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# مشکلات شناخته شده (Known Issues)
|
||||
|
||||
این فایل لیست مشکلات شناخته شده و workaround های موقت را نگه میدارد.
|
||||
|
||||
## Format
|
||||
|
||||
هر issue باید شامل:
|
||||
- عنوان
|
||||
- توضیحات
|
||||
- تاریخ کشف
|
||||
- Priority (Critical/High/Medium/Low)
|
||||
- Status (Open/In Progress/Resolved)
|
||||
- Workaround (اگر وجود دارد)
|
||||
- Related tickets
|
||||
|
||||
---
|
||||
|
||||
## فعلا مشکلی گزارش نشده
|
||||
|
||||
پروژه در مراحل اولیه است و هنوز مشکل خاصی گزارش نشده.
|
||||
|
||||
---
|
||||
|
||||
## Template برای Issue های آینده
|
||||
|
||||
```markdown
|
||||
## [ISSUE-001] عنوان مشکل
|
||||
|
||||
**تاریخ کشف**: YYYY-MM-DD
|
||||
**Priority**: Critical/High/Medium/Low
|
||||
**Status**: Open/In Progress/Resolved
|
||||
**نسخه**: X.Y.Z
|
||||
|
||||
### توضیحات
|
||||
توضیحات کامل مشکل
|
||||
|
||||
### Steps to Reproduce
|
||||
1. Step 1
|
||||
2. Step 2
|
||||
3. Step 3
|
||||
|
||||
### Expected Behavior
|
||||
رفتار مورد انتظار
|
||||
|
||||
### Actual Behavior
|
||||
رفتار واقعی
|
||||
|
||||
### Environment
|
||||
- OS: Linux/Windows/Mac
|
||||
- Python: 3.11
|
||||
- Database: PostgreSQL 14
|
||||
|
||||
### Workaround
|
||||
راه حل موقت (اگر وجود دارد)
|
||||
|
||||
### Related
|
||||
- Ticket: #123
|
||||
- PR: #456
|
||||
|
||||
### Notes
|
||||
یادداشتهای اضافی
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## مشکلات حل شده
|
||||
|
||||
(لیست مشکلات حل شده به اینجا منتقل میشود)
|
||||
|
||||
65
docs/changelog/migrations.md
Normal file
65
docs/changelog/migrations.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Database Migrations History
|
||||
|
||||
این فایل تاریخچه تمام migration های دیتابیس را نگه میدارد.
|
||||
|
||||
## Format
|
||||
|
||||
هر migration باید شامل موارد زیر باشد:
|
||||
- تاریخ
|
||||
- نام فایل migration
|
||||
- توضیحات کامل
|
||||
- جداول/ستونهای تغییر یافته
|
||||
- وابستگیها
|
||||
- دستور rollback
|
||||
|
||||
---
|
||||
|
||||
## Migrations
|
||||
|
||||
### 2025-01-24: Initial Setup
|
||||
|
||||
**Migration**: `pending`
|
||||
**Description**: آمادهسازی اولیه - هنوز migration اجرا نشده
|
||||
**Status**: Pending
|
||||
**Tables**: None yet
|
||||
**Dependencies**: None
|
||||
**Rollback**: N/A
|
||||
|
||||
**Note**: اولین migration در فاز بعدی (phase0-database) ایجاد خواهد شد.
|
||||
|
||||
---
|
||||
|
||||
## Template برای Migration های آینده
|
||||
|
||||
```markdown
|
||||
## YYYY-MM-DD: Title
|
||||
|
||||
**Migration**: `XXX_description.py`
|
||||
**Description**: توضیحات کامل تغییرات
|
||||
**Status**: Applied / Pending / Rolled Back
|
||||
**Tables**:
|
||||
- table_name_1
|
||||
- table_name_2
|
||||
|
||||
**Changes**:
|
||||
- Added column `column_name` to `table_name`
|
||||
- Created table `new_table`
|
||||
- Added index on `column_name`
|
||||
|
||||
**Dependencies**:
|
||||
- Previous migration: XXX
|
||||
|
||||
**SQL Summary**:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN DEFAULT FALSE;
|
||||
```
|
||||
|
||||
**Rollback**:
|
||||
```bash
|
||||
alembic downgrade -1
|
||||
```
|
||||
|
||||
**Tested**: Yes/No
|
||||
**Production Deploy Date**: YYYY-MM-DD
|
||||
```
|
||||
|
||||
449
docs/deployment/kubernetes.md
Normal file
449
docs/deployment/kubernetes.md
Normal file
@@ -0,0 +1,449 @@
|
||||
# راهنمای Deployment روی Kubernetes با Helm
|
||||
|
||||
این راهنما نحوه deploy کردن پلتفرم Peikarband روی Kubernetes با استفاده از Helm Chart را توضیح میدهد.
|
||||
|
||||
## پیشنیازها
|
||||
|
||||
### 1. ابزارهای مورد نیاز
|
||||
|
||||
```bash
|
||||
# Kubectl (v1.24+)
|
||||
kubectl version --client
|
||||
|
||||
# Helm (v3.10+)
|
||||
helm version
|
||||
|
||||
# Docker (برای build local)
|
||||
docker --version
|
||||
```
|
||||
|
||||
### 2. دسترسی به Kubernetes Cluster
|
||||
|
||||
```bash
|
||||
# تست دسترسی
|
||||
kubectl cluster-info
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
### 3. Namespace ها
|
||||
|
||||
```bash
|
||||
# ساخت namespace ها
|
||||
kubectl create namespace production
|
||||
kubectl create namespace staging
|
||||
```
|
||||
|
||||
## ساختار Helm Chart
|
||||
|
||||
```
|
||||
helm/peikarband/
|
||||
├── Chart.yaml # Metadata
|
||||
├── values.yaml # Default values
|
||||
├── values-production.yaml # Production overrides
|
||||
├── templates/
|
||||
│ ├── _helpers.tpl # Helper templates
|
||||
│ ├── deployment.yaml # Deployment
|
||||
│ ├── service.yaml # Service
|
||||
│ ├── ingress.yaml # Ingress
|
||||
│ ├── configmap.yaml # ConfigMap
|
||||
│ ├── serviceaccount.yaml
|
||||
│ ├── hpa.yaml # Horizontal Pod Autoscaler
|
||||
│ ├── pdb.yaml # Pod Disruption Budget
|
||||
│ ├── networkpolicy.yaml
|
||||
│ └── NOTES.txt
|
||||
└── .helmignore
|
||||
```
|
||||
|
||||
## مراحل Deployment
|
||||
|
||||
### 1. آمادهسازی Secrets
|
||||
|
||||
ابتدا باید secrets مورد نیاز را ایجاد کنید:
|
||||
|
||||
```bash
|
||||
# Database credentials
|
||||
kubectl create secret generic peikarband-secrets \
|
||||
--from-literal=db-username=peikarband \
|
||||
--from-literal=db-password=STRONG_PASSWORD_HERE \
|
||||
--from-literal=redis-password=REDIS_PASSWORD_HERE \
|
||||
-n production
|
||||
|
||||
# برای staging
|
||||
kubectl create secret generic peikarband-secrets \
|
||||
--from-literal=db-username=peikarband \
|
||||
--from-literal=db-password=STAGING_PASSWORD \
|
||||
--from-literal=redis-password=REDIS_PASSWORD \
|
||||
-n staging
|
||||
```
|
||||
|
||||
### 2. Build و Push Docker Image
|
||||
|
||||
#### روش اول: با GitHub Actions (توصیه میشود)
|
||||
|
||||
```bash
|
||||
# فقط یک tag بزنید و GitHub Actions خودکار build و deploy میکند
|
||||
git tag -a v0.1.0 -m "Release v0.1.0"
|
||||
git push origin v0.1.0
|
||||
```
|
||||
|
||||
#### روش دوم: Build دستی
|
||||
|
||||
```bash
|
||||
# Build image
|
||||
docker build -t peikarband/landing:0.1.0 .
|
||||
|
||||
# Tag for registry
|
||||
docker tag peikarband/landing:0.1.0 registry.example.com/peikarband/landing:0.1.0
|
||||
|
||||
# Push
|
||||
docker push registry.example.com/peikarband/landing:0.1.0
|
||||
```
|
||||
|
||||
### 3. Validate Helm Chart
|
||||
|
||||
قبل از deploy، chart را validate کنید:
|
||||
|
||||
```bash
|
||||
# Lint
|
||||
helm lint helm/peikarband
|
||||
|
||||
# Dry-run
|
||||
helm install peikarband-test ./helm/peikarband \
|
||||
--dry-run \
|
||||
--debug \
|
||||
--namespace production
|
||||
|
||||
# Template rendering
|
||||
helm template peikarband ./helm/peikarband > rendered.yaml
|
||||
```
|
||||
|
||||
### 4. Deploy به Staging
|
||||
|
||||
```bash
|
||||
helm upgrade --install peikarband-staging ./helm/peikarband \
|
||||
--namespace staging \
|
||||
--create-namespace \
|
||||
--set image.repository=registry.example.com/peikarband/landing \
|
||||
--set image.tag=0.1.0 \
|
||||
--set ingress.hosts[0].host=staging.peikarband.ir \
|
||||
--set replicaCount=2 \
|
||||
--wait \
|
||||
--timeout 5m
|
||||
```
|
||||
|
||||
### 5. تست Staging
|
||||
|
||||
```bash
|
||||
# چک کردن pods
|
||||
kubectl get pods -n staging
|
||||
|
||||
# چک کردن logs
|
||||
kubectl logs -f deployment/peikarband-staging -n staging
|
||||
|
||||
# Port forward برای تست local
|
||||
kubectl port-forward svc/peikarband-staging 3000:3000 -n staging
|
||||
|
||||
# تست health check
|
||||
curl http://localhost:8000/ping
|
||||
```
|
||||
|
||||
### 6. Deploy به Production
|
||||
|
||||
```bash
|
||||
helm upgrade --install peikarband-prod ./helm/peikarband \
|
||||
--namespace production \
|
||||
--create-namespace \
|
||||
--set image.repository=registry.example.com/peikarband/landing \
|
||||
--set image.tag=0.1.0 \
|
||||
--values helm/peikarband/values-production.yaml \
|
||||
--wait \
|
||||
--timeout 10m
|
||||
```
|
||||
|
||||
## پیکربندیهای مهم
|
||||
|
||||
### 1. تغییر تعداد Replicas
|
||||
|
||||
```bash
|
||||
# با Helm
|
||||
helm upgrade peikarband-prod ./helm/peikarband \
|
||||
--namespace production \
|
||||
--reuse-values \
|
||||
--set replicaCount=5
|
||||
|
||||
# یا با kubectl
|
||||
kubectl scale deployment peikarband-prod --replicas=5 -n production
|
||||
```
|
||||
|
||||
### 2. Update Image Version
|
||||
|
||||
```bash
|
||||
helm upgrade peikarband-prod ./helm/peikarband \
|
||||
--namespace production \
|
||||
--reuse-values \
|
||||
--set image.tag=0.2.0
|
||||
```
|
||||
|
||||
### 3. تغییر Resources
|
||||
|
||||
```bash
|
||||
helm upgrade peikarband-prod ./helm/peikarband \
|
||||
--namespace production \
|
||||
--reuse-values \
|
||||
--set resources.limits.cpu=2000m \
|
||||
--set resources.limits.memory=2Gi
|
||||
```
|
||||
|
||||
### 4. فعال/غیرفعال کردن Autoscaling
|
||||
|
||||
```bash
|
||||
# فعال کردن
|
||||
helm upgrade peikarband-prod ./helm/peikarband \
|
||||
--namespace production \
|
||||
--reuse-values \
|
||||
--set autoscaling.enabled=true \
|
||||
--set autoscaling.minReplicas=3 \
|
||||
--set autoscaling.maxReplicas=10
|
||||
|
||||
# غیرفعال کردن
|
||||
helm upgrade peikarband-prod ./helm/peikarband \
|
||||
--namespace production \
|
||||
--reuse-values \
|
||||
--set autoscaling.enabled=false \
|
||||
--set replicaCount=3
|
||||
```
|
||||
|
||||
## Ingress و SSL/TLS
|
||||
|
||||
### نصب cert-manager (برای Let's Encrypt)
|
||||
|
||||
```bash
|
||||
# نصب cert-manager
|
||||
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
|
||||
|
||||
# ساخت ClusterIssuer
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: admin@peikarband.ir
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: nginx
|
||||
EOF
|
||||
```
|
||||
|
||||
### پیکربندی DNS
|
||||
|
||||
```bash
|
||||
# A Record برای domain اصلی
|
||||
peikarband.ir. A YOUR_CLUSTER_IP
|
||||
|
||||
# CNAME برای www
|
||||
www.peikarband.ir. CNAME peikarband.ir.
|
||||
```
|
||||
|
||||
## Monitoring و Logs
|
||||
|
||||
### 1. مشاهده Logs
|
||||
|
||||
```bash
|
||||
# تمام pods
|
||||
kubectl logs -f deployment/peikarband-prod -n production
|
||||
|
||||
# یک pod خاص
|
||||
kubectl logs -f peikarband-prod-xxxxx-yyyyy -n production
|
||||
|
||||
# تمام logs (از همه pods)
|
||||
kubectl logs -l app.kubernetes.io/name=peikarband -n production --tail=100
|
||||
```
|
||||
|
||||
### 2. مشاهده Events
|
||||
|
||||
```bash
|
||||
kubectl get events -n production --sort-by='.lastTimestamp'
|
||||
```
|
||||
|
||||
### 3. چک کردن Resource Usage
|
||||
|
||||
```bash
|
||||
# CPU و Memory
|
||||
kubectl top pods -n production
|
||||
|
||||
# Metrics از deployment
|
||||
kubectl top deployment peikarband-prod -n production
|
||||
```
|
||||
|
||||
### 4. HPA Status
|
||||
|
||||
```bash
|
||||
kubectl get hpa -n production
|
||||
kubectl describe hpa peikarband-prod -n production
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
### 1. مشاهده History
|
||||
|
||||
```bash
|
||||
# Helm releases
|
||||
helm history peikarband-prod -n production
|
||||
|
||||
# Kubernetes rollout history
|
||||
kubectl rollout history deployment/peikarband-prod -n production
|
||||
```
|
||||
|
||||
### 2. Rollback با Helm
|
||||
|
||||
```bash
|
||||
# به نسخه قبلی
|
||||
helm rollback peikarband-prod -n production
|
||||
|
||||
# به نسخه خاص
|
||||
helm rollback peikarband-prod 3 -n production
|
||||
```
|
||||
|
||||
### 3. Rollback با Kubectl
|
||||
|
||||
```bash
|
||||
# به نسخه قبلی
|
||||
kubectl rollout undo deployment/peikarband-prod -n production
|
||||
|
||||
# به نسخه خاص
|
||||
kubectl rollout undo deployment/peikarband-prod --to-revision=2 -n production
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Pod در حالت Pending
|
||||
|
||||
```bash
|
||||
# بررسی events
|
||||
kubectl describe pod POD_NAME -n production
|
||||
|
||||
# چک کردن resources
|
||||
kubectl describe nodes
|
||||
```
|
||||
|
||||
### Pod در حالت CrashLoopBackOff
|
||||
|
||||
```bash
|
||||
# مشاهده logs
|
||||
kubectl logs POD_NAME -n production --previous
|
||||
|
||||
# مشاهده events
|
||||
kubectl describe pod POD_NAME -n production
|
||||
```
|
||||
|
||||
### Image Pull Error
|
||||
|
||||
```bash
|
||||
# چک کردن imagePullSecrets
|
||||
kubectl get secrets -n production
|
||||
|
||||
# بررسی pod
|
||||
kubectl describe pod POD_NAME -n production
|
||||
```
|
||||
|
||||
### Health Check Failing
|
||||
|
||||
```bash
|
||||
# تست مستقیم health endpoint
|
||||
kubectl exec -it POD_NAME -n production -- curl localhost:8000/ping
|
||||
|
||||
# بررسی liveness/readiness probes
|
||||
kubectl describe pod POD_NAME -n production
|
||||
```
|
||||
|
||||
## Clean Up
|
||||
|
||||
### حذف Release
|
||||
|
||||
```bash
|
||||
# حذف کامل
|
||||
helm uninstall peikarband-prod -n production
|
||||
|
||||
# با نگه داشتن history
|
||||
helm uninstall peikarband-prod -n production --keep-history
|
||||
```
|
||||
|
||||
### حذف Namespace
|
||||
|
||||
```bash
|
||||
kubectl delete namespace production
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always use specific image tags
|
||||
```yaml
|
||||
image:
|
||||
tag: "v0.1.0" # ✅ Good
|
||||
# tag: "latest" # ❌ Bad
|
||||
```
|
||||
|
||||
### 2. Set resource limits
|
||||
```yaml
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
```
|
||||
|
||||
### 3. Enable autoscaling برای production
|
||||
```yaml
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
```
|
||||
|
||||
### 4. استفاده از Pod Disruption Budget
|
||||
```yaml
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
minAvailable: 1
|
||||
```
|
||||
|
||||
### 5. NetworkPolicy برای امنیت
|
||||
```yaml
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
workflow ما به صورت خودکار:
|
||||
|
||||
1. ✅ Build Docker image
|
||||
2. ✅ Push به registry
|
||||
3. ✅ Package Helm chart
|
||||
4. ✅ Deploy به staging (on push to main)
|
||||
5. ✅ Deploy به production (on tag)
|
||||
6. ✅ Create GitHub release
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
```bash
|
||||
# از طریق GitHub UI: Actions → CD → Run workflow
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
برای مشکلات و سوالات:
|
||||
- 📧 Email: support@peikarband.ir
|
||||
- 💬 Telegram: @peikarband_support
|
||||
- 📚 Docs: https://docs.peikarband.ir
|
||||
|
||||
104
docs/deployment/quickstart.md
Normal file
104
docs/deployment/quickstart.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Quick Start - Deploy در 5 دقیقه
|
||||
|
||||
راهنمای سریع برای deploy کردن Peikarband روی Kubernetes.
|
||||
|
||||
## پیشنیاز
|
||||
|
||||
- Kubernetes cluster (v1.24+)
|
||||
- Helm 3 نصب شده
|
||||
- kubectl پیکربندی شده
|
||||
- دسترسی به یک Container Registry
|
||||
|
||||
## مراحل
|
||||
|
||||
### 1. Clone Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/peikarband/landing.git
|
||||
cd landing
|
||||
```
|
||||
|
||||
### 2. Build Docker Image
|
||||
|
||||
```bash
|
||||
# تغییر REGISTRY به registry خودتون
|
||||
export REGISTRY=registry.example.com
|
||||
export IMAGE_NAME=peikarband/landing
|
||||
export VERSION=0.1.0
|
||||
|
||||
docker build -t ${REGISTRY}/${IMAGE_NAME}:${VERSION} .
|
||||
docker push ${REGISTRY}/${IMAGE_NAME}:${VERSION}
|
||||
```
|
||||
|
||||
### 3. ساخت Secrets
|
||||
|
||||
```bash
|
||||
kubectl create namespace production
|
||||
|
||||
kubectl create secret generic peikarband-secrets \
|
||||
--from-literal=db-username=peikarband \
|
||||
--from-literal=db-password=YOUR_STRONG_PASSWORD \
|
||||
--from-literal=redis-password=YOUR_REDIS_PASSWORD \
|
||||
-n production
|
||||
```
|
||||
|
||||
### 4. Deploy با Helm
|
||||
|
||||
```bash
|
||||
helm upgrade --install peikarband ./helm/peikarband \
|
||||
--namespace production \
|
||||
--set image.repository=${REGISTRY}/${IMAGE_NAME} \
|
||||
--set image.tag=${VERSION} \
|
||||
--set ingress.hosts[0].host=yourdomain.com \
|
||||
--wait
|
||||
```
|
||||
|
||||
### 5. چک کردن وضعیت
|
||||
|
||||
```bash
|
||||
# Pods
|
||||
kubectl get pods -n production
|
||||
|
||||
# Service
|
||||
kubectl get svc -n production
|
||||
|
||||
# Ingress
|
||||
kubectl get ingress -n production
|
||||
```
|
||||
|
||||
### 6. دسترسی به Application
|
||||
|
||||
```bash
|
||||
# Port forward برای تست
|
||||
kubectl port-forward svc/peikarband 3000:3000 -n production
|
||||
|
||||
# باز کردن در browser
|
||||
open http://localhost:3000
|
||||
```
|
||||
|
||||
## پیکربندی Production
|
||||
|
||||
برای production از `values-production.yaml` استفاده کنید:
|
||||
|
||||
```bash
|
||||
helm upgrade --install peikarband ./helm/peikarband \
|
||||
--namespace production \
|
||||
--set image.repository=${REGISTRY}/${IMAGE_NAME} \
|
||||
--set image.tag=${VERSION} \
|
||||
--values helm/peikarband/values-production.yaml \
|
||||
--wait
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
```bash
|
||||
helm uninstall peikarband -n production
|
||||
kubectl delete namespace production
|
||||
```
|
||||
|
||||
## بعدش چی؟
|
||||
|
||||
- [مستندات کامل Kubernetes](./kubernetes.md)
|
||||
- [راهنمای CI/CD](../development/ci-cd.md)
|
||||
- [Troubleshooting](./troubleshooting.md)
|
||||
|
||||
386
docs/development/coding-standards.md
Normal file
386
docs/development/coding-standards.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# استانداردهای کدنویسی
|
||||
|
||||
## اصول کلی
|
||||
|
||||
### 1. کد تمیز (Clean Code)
|
||||
|
||||
کد باید:
|
||||
- خوانا باشد
|
||||
- ساده باشد
|
||||
- قابل فهم باشد
|
||||
- قابل نگهداری باشد
|
||||
|
||||
### 2. PEP 8 Compliance
|
||||
|
||||
**الزامی**: تمام کدها باید با PEP 8 مطابقت داشته باشند.
|
||||
|
||||
استفاده از ابزارها:
|
||||
```bash
|
||||
black src/ # Auto-formatting
|
||||
flake8 src/ # Linting
|
||||
isort src/ # Import sorting
|
||||
```
|
||||
|
||||
## قوانین Naming
|
||||
|
||||
### Classes
|
||||
|
||||
```python
|
||||
# ✅ GOOD: PascalCase
|
||||
class UserService:
|
||||
pass
|
||||
|
||||
class PaymentGateway:
|
||||
pass
|
||||
|
||||
class DatabaseConnection:
|
||||
pass
|
||||
|
||||
# ❌ BAD
|
||||
class user_service: # Wrong case
|
||||
class payment_gateway_service: # Too long
|
||||
class UServ: # Unclear abbreviation
|
||||
```
|
||||
|
||||
### Functions & Methods
|
||||
|
||||
```python
|
||||
# ✅ GOOD: snake_case, descriptive
|
||||
def calculate_total_price(items: List[Item]) -> Decimal:
|
||||
pass
|
||||
|
||||
def send_welcome_email(user: User) -> bool:
|
||||
pass
|
||||
|
||||
# ❌ BAD
|
||||
def calcTotPrice(): # Mixed case
|
||||
def cp(): # Not descriptive
|
||||
def calculate(): # Too vague
|
||||
```
|
||||
|
||||
### Variables
|
||||
|
||||
```python
|
||||
# ✅ GOOD
|
||||
user_email = "test@example.com"
|
||||
total_amount = Decimal("100.00")
|
||||
is_active = True
|
||||
MAX_ATTEMPTS = 3 # Constant
|
||||
|
||||
# ❌ BAD
|
||||
e = "test@example.com" # Too short
|
||||
userEmail = "test@example.com" # camelCase
|
||||
TOTAL = 100 # Constant style for variable
|
||||
```
|
||||
|
||||
## Type Hints
|
||||
|
||||
**الزامی**: همه function signatures باید type hints داشته باشند.
|
||||
|
||||
```python
|
||||
from typing import Optional, List, Dict, Any
|
||||
from decimal import Decimal
|
||||
|
||||
# ✅ GOOD
|
||||
def get_user(user_id: int) -> Optional[User]:
|
||||
pass
|
||||
|
||||
def create_invoice(
|
||||
user_id: int,
|
||||
amount: Decimal,
|
||||
items: List[InvoiceItem]
|
||||
) -> Invoice:
|
||||
pass
|
||||
|
||||
# ❌ BAD
|
||||
def get_user(user_id): # No types
|
||||
pass
|
||||
|
||||
def create_invoice(user_id, amount, items): # No types
|
||||
pass
|
||||
```
|
||||
|
||||
## Docstrings
|
||||
|
||||
**الزامی**: همه public functions, classes, و methods باید docstring داشته باشند.
|
||||
|
||||
### Google Style (استاندارد پروژه)
|
||||
|
||||
```python
|
||||
def create_user(
|
||||
email: str,
|
||||
password: str,
|
||||
full_name: str
|
||||
) -> User:
|
||||
"""Create a new user account.
|
||||
|
||||
Args:
|
||||
email: User's email address
|
||||
password: Plain text password
|
||||
full_name: User's full name
|
||||
|
||||
Returns:
|
||||
User: Created user object
|
||||
|
||||
Raises:
|
||||
EmailAlreadyExistsException: If email exists
|
||||
WeakPasswordException: If password is weak
|
||||
|
||||
Example:
|
||||
>>> user = create_user(
|
||||
... email="test@example.com",
|
||||
... password="SecurePass123!",
|
||||
... full_name="John Doe"
|
||||
... )
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Use Specific Exceptions
|
||||
|
||||
```python
|
||||
# ✅ GOOD
|
||||
try:
|
||||
user = user_repository.get_by_id(user_id)
|
||||
if not user:
|
||||
raise UserNotFoundException(f"User {user_id} not found")
|
||||
except DatabaseException as e:
|
||||
logger.error("database_error", error=str(e))
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.critical("unexpected_error", error=str(e))
|
||||
raise
|
||||
|
||||
# ❌ BAD
|
||||
try:
|
||||
user = get_user(user_id)
|
||||
except: # Never use bare except!
|
||||
pass
|
||||
|
||||
try:
|
||||
user = get_user(user_id)
|
||||
except Exception: # Too broad
|
||||
pass
|
||||
```
|
||||
|
||||
### Custom Exception Hierarchy
|
||||
|
||||
```python
|
||||
class PeikarbandException(Exception):
|
||||
"""Base exception."""
|
||||
pass
|
||||
|
||||
class DomainException(PeikarbandException):
|
||||
"""Domain layer exceptions."""
|
||||
pass
|
||||
|
||||
class UserNotFoundException(DomainException):
|
||||
"""User not found."""
|
||||
pass
|
||||
```
|
||||
|
||||
## Code Organization
|
||||
|
||||
### Imports
|
||||
|
||||
```python
|
||||
# 1. Standard library
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional, List
|
||||
from decimal import Decimal
|
||||
|
||||
# 2. Third-party
|
||||
import redis
|
||||
from sqlalchemy import Column, Integer
|
||||
from pydantic import BaseModel
|
||||
|
||||
# 3. Local
|
||||
from src.config.settings import settings
|
||||
from src.core.domain.entities.user import User
|
||||
```
|
||||
|
||||
### Function Length
|
||||
|
||||
**Max 50 lines per function** (توصیه: 20-30 lines)
|
||||
|
||||
```python
|
||||
# ✅ GOOD: Short and focused
|
||||
def calculate_discount(amount: Decimal, user: User) -> Decimal:
|
||||
"""Calculate user discount."""
|
||||
if user.is_premium:
|
||||
return amount * Decimal("0.10")
|
||||
return Decimal("0.00")
|
||||
|
||||
# ❌ BAD: Too long (100+ lines)
|
||||
def process_order(order_data):
|
||||
# 100+ lines of code
|
||||
pass
|
||||
```
|
||||
|
||||
### Class Length
|
||||
|
||||
**Max 300 lines per class** (توصیه: 100-200 lines)
|
||||
|
||||
اگر class بزرگ شد، به چند class کوچکتر تقسیم کنید.
|
||||
|
||||
## Comments
|
||||
|
||||
### When to Comment
|
||||
|
||||
```python
|
||||
# ✅ GOOD: Explain WHY, not WHAT
|
||||
def calculate_tax(amount: Decimal) -> Decimal:
|
||||
# Iranian tax rate is 9% as of 2024
|
||||
TAX_RATE = Decimal("0.09")
|
||||
return amount * TAX_RATE
|
||||
|
||||
# ❌ BAD: States the obvious
|
||||
def add(a, b):
|
||||
# Add two numbers
|
||||
return a + b
|
||||
```
|
||||
|
||||
### TODO Comments
|
||||
|
||||
```python
|
||||
# TODO(username): Description of what needs to be done
|
||||
# TODO(john): Implement caching for this query
|
||||
# FIXME(jane): This breaks when amount is negative
|
||||
# HACK(bob): Temporary workaround until API is fixed
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Single Responsibility
|
||||
|
||||
```python
|
||||
# ✅ GOOD
|
||||
class UserRepository:
|
||||
def save(self, user: User) -> User:
|
||||
pass
|
||||
|
||||
class EmailService:
|
||||
def send_email(self, to: str, subject: str) -> bool:
|
||||
pass
|
||||
|
||||
# ❌ BAD
|
||||
class UserManager:
|
||||
def save_user(self, user):
|
||||
pass
|
||||
|
||||
def send_email(self, user):
|
||||
pass
|
||||
|
||||
def log_activity(self, user):
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. DRY (Don't Repeat Yourself)
|
||||
|
||||
```python
|
||||
# ❌ BAD
|
||||
def get_user_name(user_id):
|
||||
db = connect_db()
|
||||
user = db.query(User).filter_by(id=user_id).first()
|
||||
db.close()
|
||||
return user.name
|
||||
|
||||
def get_user_email(user_id):
|
||||
db = connect_db()
|
||||
user = db.query(User).filter_by(id=user_id).first()
|
||||
db.close()
|
||||
return user.email
|
||||
|
||||
# ✅ GOOD
|
||||
def get_user(user_id: int) -> Optional[User]:
|
||||
with get_db_context() as db:
|
||||
return db.query(User).filter_by(id=user_id).first()
|
||||
|
||||
def get_user_name(user_id: int) -> Optional[str]:
|
||||
user = get_user(user_id)
|
||||
return user.name if user else None
|
||||
```
|
||||
|
||||
### 3. Early Return
|
||||
|
||||
```python
|
||||
# ✅ GOOD: Early return
|
||||
def process_payment(amount: Decimal) -> bool:
|
||||
if amount <= 0:
|
||||
return False
|
||||
|
||||
if not user.has_sufficient_balance(amount):
|
||||
return False
|
||||
|
||||
# Process payment
|
||||
return True
|
||||
|
||||
# ❌ BAD: Nested conditions
|
||||
def process_payment(amount):
|
||||
if amount > 0:
|
||||
if user.has_sufficient_balance(amount):
|
||||
# Process payment
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
### 4. Use Constants
|
||||
|
||||
```python
|
||||
# ✅ GOOD
|
||||
MAX_LOGIN_ATTEMPTS = 3
|
||||
PASSWORD_MIN_LENGTH = 8
|
||||
SESSION_TIMEOUT_MINUTES = 30
|
||||
|
||||
if attempts >= MAX_LOGIN_ATTEMPTS:
|
||||
lock_account()
|
||||
|
||||
# ❌ BAD: Magic numbers
|
||||
if attempts >= 3: # What's 3?
|
||||
lock_account()
|
||||
```
|
||||
|
||||
### 5. Avoid Deep Nesting
|
||||
|
||||
```python
|
||||
# ✅ GOOD: Max 2-3 levels
|
||||
def process_order(order: Order) -> bool:
|
||||
if not order.is_valid():
|
||||
return False
|
||||
|
||||
if not user.can_order():
|
||||
return False
|
||||
|
||||
return save_order(order)
|
||||
|
||||
# ❌ BAD: Too nested
|
||||
def process_order(order):
|
||||
if order:
|
||||
if order.is_valid():
|
||||
if user:
|
||||
if user.can_order():
|
||||
if save_order(order):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
قبل از ارسال PR، این موارد را بررسی کنید:
|
||||
|
||||
- [ ] همه تستها pass میشوند
|
||||
- [ ] Code coverage کافی است (>80%)
|
||||
- [ ] تمام functions دارای type hints هستند
|
||||
- [ ] تمام public functions دارای docstring هستند
|
||||
- [ ] black, flake8, mypy بدون error
|
||||
- [ ] هیچ TODO/FIXME جدید بدون توضیح نیست
|
||||
- [ ] تغییرات در CHANGELOG.md ثبت شده
|
||||
- [ ] مستندات بهروز شده
|
||||
|
||||
---
|
||||
|
||||
**الزامات**: این استانداردها الزامی هستند و در code review بررسی میشوند.
|
||||
|
||||
433
docs/development/git-workflow.md
Normal file
433
docs/development/git-workflow.md
Normal file
@@ -0,0 +1,433 @@
|
||||
# Git Workflow
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
### Main Branches
|
||||
|
||||
```
|
||||
main (production)
|
||||
└── develop (staging)
|
||||
```
|
||||
|
||||
- **main**: Production-ready code
|
||||
- **develop**: Integration branch for features
|
||||
|
||||
### Supporting Branches
|
||||
|
||||
```
|
||||
develop
|
||||
├── feature/user-authentication
|
||||
├── feature/billing-system
|
||||
├── bugfix/payment-timeout
|
||||
└── hotfix/security-patch
|
||||
```
|
||||
|
||||
## Branch Naming Convention
|
||||
|
||||
### Feature Branches
|
||||
```
|
||||
feature/short-description
|
||||
feature/user-auth
|
||||
feature/payment-gateway
|
||||
```
|
||||
|
||||
### Bugfix Branches
|
||||
```
|
||||
bugfix/issue-description
|
||||
bugfix/payment-timeout
|
||||
bugfix/email-sending
|
||||
```
|
||||
|
||||
### Hotfix Branches
|
||||
```
|
||||
hotfix/critical-issue
|
||||
hotfix/security-vuln
|
||||
hotfix/data-loss
|
||||
```
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
### Structure
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
### Type
|
||||
|
||||
- `feat`: ویژگی جدید
|
||||
- `fix`: رفع باگ
|
||||
- `docs`: تغییرات مستندات
|
||||
- `style`: فرمتبندی، فاصلهگذاری
|
||||
- `refactor`: بازنویسی کد
|
||||
- `perf`: بهبود performance
|
||||
- `test`: اضافه/اصلاح تست
|
||||
- `chore`: کارهای نگهداری
|
||||
- `build`: تغییرات build system
|
||||
- `ci`: تغییرات CI/CD
|
||||
|
||||
### Scope
|
||||
|
||||
بخشی از کد که تغییر کرده:
|
||||
- `auth`
|
||||
- `billing`
|
||||
- `api`
|
||||
- `database`
|
||||
- `ui`
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
feat(auth): add two-factor authentication
|
||||
|
||||
- Implement TOTP-based 2FA
|
||||
- Add QR code generation
|
||||
- Add backup codes
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
```bash
|
||||
fix(payment): resolve timeout issue with zarinpal
|
||||
|
||||
The payment gateway was timing out due to long response time.
|
||||
Added retry logic with exponential backoff.
|
||||
|
||||
Fixes #456
|
||||
```
|
||||
|
||||
```bash
|
||||
docs(api): update authentication endpoints
|
||||
|
||||
Added examples for JWT token usage and refresh flow.
|
||||
```
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
### 1. شروع کار روی Feature جدید
|
||||
|
||||
```bash
|
||||
# بروزرسانی develop
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
|
||||
# ایجاد branch جدید
|
||||
git checkout -b feature/my-feature
|
||||
|
||||
# شروع کدنویسی...
|
||||
```
|
||||
|
||||
### 2. Commit های منظم
|
||||
|
||||
```bash
|
||||
# بررسی تغییرات
|
||||
git status
|
||||
git diff
|
||||
|
||||
# Stage کردن
|
||||
git add src/specific/file.py
|
||||
# یا
|
||||
git add .
|
||||
|
||||
# Commit
|
||||
git commit -m "feat(scope): description"
|
||||
|
||||
# Push (اولین بار)
|
||||
git push -u origin feature/my-feature
|
||||
|
||||
# Push های بعدی
|
||||
git push
|
||||
```
|
||||
|
||||
### 3. نگهداری Branch بهروز
|
||||
|
||||
```bash
|
||||
# بروزرسانی از develop
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
|
||||
git checkout feature/my-feature
|
||||
git merge develop
|
||||
|
||||
# حل conflict ها (در صورت وجود)
|
||||
# ... edit files ...
|
||||
git add .
|
||||
git commit -m "merge: resolve conflicts with develop"
|
||||
```
|
||||
|
||||
### 4. ایجاد Pull Request
|
||||
|
||||
1. Push کردن همه commits
|
||||
2. رفتن به GitHub/GitLab
|
||||
3. Create Pull Request
|
||||
4. انتخاب base: `develop`, compare: `feature/my-feature`
|
||||
5. عنوان و توضیحات
|
||||
6. Request reviewers
|
||||
7. Link کردن issues
|
||||
|
||||
### 5. Code Review Process
|
||||
|
||||
**برای نویسنده**:
|
||||
- پاسخ به comments
|
||||
- انجام تغییرات درخواستی
|
||||
- Push کردن updates
|
||||
- Request re-review
|
||||
|
||||
**برای Reviewer**:
|
||||
- بررسی کد با دقت
|
||||
- Check کردن tests
|
||||
- بررسی code quality
|
||||
- Approve یا Request Changes
|
||||
|
||||
### 6. Merge
|
||||
|
||||
پس از approval:
|
||||
```bash
|
||||
# Squash and Merge (توصیه میشود)
|
||||
# یا
|
||||
# Merge Commit
|
||||
# یا
|
||||
# Rebase and Merge
|
||||
```
|
||||
|
||||
### 7. پاک کردن Branch
|
||||
|
||||
```bash
|
||||
# Local
|
||||
git branch -d feature/my-feature
|
||||
|
||||
# Remote (معمولا automatic)
|
||||
git push origin --delete feature/my-feature
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Commit های کوچک و مشخص
|
||||
|
||||
```bash
|
||||
# ✅ GOOD: Small, focused commits
|
||||
git commit -m "feat(auth): add email validation"
|
||||
git commit -m "test(auth): add tests for email validation"
|
||||
git commit -m "docs(auth): document email validation"
|
||||
|
||||
# ❌ BAD: Large, vague commit
|
||||
git commit -m "add stuff"
|
||||
```
|
||||
|
||||
### Commit منظم
|
||||
|
||||
```bash
|
||||
# Commit هر چند ساعت یکبار
|
||||
# نه خیلی کم (هر خط کد)
|
||||
# نه خیلی زیاد (روزی یک commit)
|
||||
```
|
||||
|
||||
### Push روزانه
|
||||
|
||||
```bash
|
||||
# حداقل یکبار در روز push کنید
|
||||
# از دست رفتن کد جلوگیری میکند
|
||||
```
|
||||
|
||||
### استفاده از .gitignore
|
||||
|
||||
```bash
|
||||
# اضافه نکردن فایلهای غیرضروری
|
||||
# - __pycache__
|
||||
# - .env
|
||||
# - venv/
|
||||
# - *.pyc
|
||||
```
|
||||
|
||||
## Git Commands Reference
|
||||
|
||||
### Basic
|
||||
|
||||
```bash
|
||||
# Status
|
||||
git status
|
||||
|
||||
# Log
|
||||
git log
|
||||
git log --oneline
|
||||
git log --graph
|
||||
|
||||
# Diff
|
||||
git diff
|
||||
git diff --staged
|
||||
```
|
||||
|
||||
### Branching
|
||||
|
||||
```bash
|
||||
# لیست branches
|
||||
git branch
|
||||
git branch -a # با remote
|
||||
|
||||
# ایجاد branch
|
||||
git branch feature/new-feature
|
||||
git checkout -b feature/new-feature # ایجاد + checkout
|
||||
|
||||
# تغییر branch
|
||||
git checkout develop
|
||||
|
||||
# حذف branch
|
||||
git branch -d feature/old-feature
|
||||
git branch -D feature/force-delete # Force
|
||||
```
|
||||
|
||||
### Stashing
|
||||
|
||||
```bash
|
||||
# ذخیره تغییرات موقت
|
||||
git stash
|
||||
|
||||
# با پیام
|
||||
git stash save "work in progress"
|
||||
|
||||
# لیست stash ها
|
||||
git stash list
|
||||
|
||||
# برگرداندن stash
|
||||
git stash apply
|
||||
git stash pop # apply + delete
|
||||
|
||||
# حذف stash
|
||||
git stash drop
|
||||
git stash clear # همه
|
||||
```
|
||||
|
||||
### Undoing Changes
|
||||
|
||||
```bash
|
||||
# Unstage file
|
||||
git reset HEAD file.py
|
||||
|
||||
# Discard changes
|
||||
git checkout -- file.py
|
||||
|
||||
# Undo last commit (keep changes)
|
||||
git reset --soft HEAD~1
|
||||
|
||||
# Undo last commit (discard changes)
|
||||
git reset --hard HEAD~1
|
||||
|
||||
# Revert commit (safe)
|
||||
git revert <commit-hash>
|
||||
```
|
||||
|
||||
### Rebasing
|
||||
|
||||
```bash
|
||||
# Rebase on develop
|
||||
git checkout feature/my-feature
|
||||
git rebase develop
|
||||
|
||||
# Interactive rebase
|
||||
git rebase -i HEAD~3 # آخرین 3 commits
|
||||
|
||||
# Abort rebase
|
||||
git rebase --abort
|
||||
|
||||
# Continue after resolving conflicts
|
||||
git rebase --continue
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
```bash
|
||||
# ایجاد tag
|
||||
git tag v1.0.0
|
||||
|
||||
# با پیام
|
||||
git tag -a v1.0.0 -m "Release version 1.0.0"
|
||||
|
||||
# Push tags
|
||||
git push origin v1.0.0
|
||||
git push origin --tags # همه tags
|
||||
|
||||
# حذف tag
|
||||
git tag -d v1.0.0
|
||||
git push origin --delete v1.0.0
|
||||
```
|
||||
|
||||
## Merge Conflicts
|
||||
|
||||
### حل Conflict
|
||||
|
||||
```bash
|
||||
# 1. مشاهده conflicts
|
||||
git status
|
||||
|
||||
# 2. باز کردن فایل و حل conflict
|
||||
# <<<<<<< HEAD
|
||||
# کد شما
|
||||
# =======
|
||||
# کد دیگران
|
||||
# >>>>>>> branch-name
|
||||
|
||||
# 3. Stage کردن
|
||||
git add resolved-file.py
|
||||
|
||||
# 4. Commit
|
||||
git commit -m "merge: resolve conflicts"
|
||||
```
|
||||
|
||||
### ابزارهای کمکی
|
||||
|
||||
```bash
|
||||
# استفاده از merge tool
|
||||
git mergetool
|
||||
|
||||
# استفاده از --ours یا --theirs
|
||||
git checkout --ours file.py
|
||||
git checkout --theirs file.py
|
||||
```
|
||||
|
||||
## Tips & Tricks
|
||||
|
||||
### Aliases
|
||||
|
||||
```bash
|
||||
# در ~/.gitconfig
|
||||
[alias]
|
||||
co = checkout
|
||||
br = branch
|
||||
ci = commit
|
||||
st = status
|
||||
lg = log --oneline --graph --decorate
|
||||
```
|
||||
|
||||
### Commit Templates
|
||||
|
||||
```bash
|
||||
# در ~/.gitmessage
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
```bash
|
||||
git config --global commit.template ~/.gitmessage
|
||||
```
|
||||
|
||||
### Auto-completion
|
||||
|
||||
```bash
|
||||
# Bash
|
||||
source /usr/share/bash-completion/completions/git
|
||||
|
||||
# Zsh
|
||||
autoload -Uz compinit && compinit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**مهم**: این workflow الزامی است و در code review بررسی میشود.
|
||||
|
||||
371
docs/development/setup.md
Normal file
371
docs/development/setup.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# راهنمای راهاندازی محیط توسعه
|
||||
|
||||
## پیشنیازها
|
||||
|
||||
### نرمافزارهای مورد نیاز
|
||||
|
||||
| نرمافزار | نسخه مورد نیاز | لینک نصب |
|
||||
|-----------|----------------|----------|
|
||||
| Python | 3.11+ | [python.org](https://python.org) |
|
||||
| PostgreSQL | 14+ | [postgresql.org](https://postgresql.org) |
|
||||
| Redis | 7+ | [redis.io](https://redis.io) |
|
||||
| Node.js | 18+ | [nodejs.org](https://nodejs.org) |
|
||||
| Git | 2.x+ | [git-scm.com](https://git-scm.com) |
|
||||
|
||||
### بررسی نسخهها
|
||||
|
||||
```bash
|
||||
python --version # باید 3.11 یا بالاتر
|
||||
psql --version # باید 14 یا بالاتر
|
||||
redis-cli --version # باید 7 یا بالاتر
|
||||
node --version # باید 18 یا بالاتر
|
||||
git --version
|
||||
```
|
||||
|
||||
## نصب
|
||||
|
||||
### 1. Clone Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/peikarband.git
|
||||
cd peikarband
|
||||
```
|
||||
|
||||
### 2. Virtual Environment
|
||||
|
||||
```bash
|
||||
# ایجاد virtual environment
|
||||
python -m venv venv
|
||||
|
||||
# فعالسازی
|
||||
# Linux/Mac:
|
||||
source venv/bin/activate
|
||||
|
||||
# Windows:
|
||||
venv\Scripts\activate
|
||||
```
|
||||
|
||||
### 3. نصب Dependencies
|
||||
|
||||
```bash
|
||||
# Core dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Development dependencies
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
# تایید نصب
|
||||
pip list
|
||||
```
|
||||
|
||||
### 4. Pre-commit Hooks
|
||||
|
||||
```bash
|
||||
# نصب pre-commit hooks
|
||||
pre-commit install
|
||||
|
||||
# تست (اختیاری)
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
## تنظیمات
|
||||
|
||||
### 1. Environment Variables
|
||||
|
||||
```bash
|
||||
# کپی فایل example
|
||||
cp .env.example .env
|
||||
|
||||
# ویرایش .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
**متغیرهای ضروری**:
|
||||
|
||||
```env
|
||||
# Database
|
||||
DATABASE_URL=postgresql://username:password@localhost:5432/peikarband
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# Security
|
||||
SECRET_KEY=generate-a-secure-random-key
|
||||
JWT_SECRET_KEY=generate-another-secure-random-key
|
||||
|
||||
# Celery
|
||||
CELERY_BROKER_URL=redis://localhost:6379/1
|
||||
CELERY_RESULT_BACKEND=redis://localhost:6379/2
|
||||
```
|
||||
|
||||
**تولید Secret Key**:
|
||||
```bash
|
||||
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
```
|
||||
|
||||
### 2. PostgreSQL Setup
|
||||
|
||||
```bash
|
||||
# ایجاد دیتابیس
|
||||
createdb peikarband
|
||||
|
||||
# یا با psql
|
||||
psql -U postgres
|
||||
CREATE DATABASE peikarband;
|
||||
CREATE USER peikarband_user WITH PASSWORD 'your_password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE peikarband TO peikarband_user;
|
||||
\q
|
||||
```
|
||||
|
||||
### 3. Redis Setup
|
||||
|
||||
```bash
|
||||
# شروع Redis
|
||||
redis-server
|
||||
|
||||
# تست
|
||||
redis-cli ping
|
||||
# باید "PONG" برگرداند
|
||||
```
|
||||
|
||||
### 4. Database Migrations
|
||||
|
||||
```bash
|
||||
# اجرای migrations
|
||||
alembic upgrade head
|
||||
|
||||
# بررسی
|
||||
alembic current
|
||||
```
|
||||
|
||||
### 5. Seed Database (اختیاری)
|
||||
|
||||
```bash
|
||||
python scripts/seed_database.py
|
||||
```
|
||||
|
||||
## اجرای پروژه
|
||||
|
||||
### Development Mode
|
||||
|
||||
```bash
|
||||
# روش 1: با Reflex
|
||||
python -m reflex run
|
||||
|
||||
# روش 2: با auto-reload
|
||||
python -m reflex run --reload
|
||||
|
||||
# روش 3: با loglevel
|
||||
python -m reflex run --loglevel debug
|
||||
```
|
||||
|
||||
### Background Services
|
||||
|
||||
**Terminal 1**: Celery Worker
|
||||
```bash
|
||||
celery -A src.infrastructure.tasks.celery_app worker -l info
|
||||
```
|
||||
|
||||
**Terminal 2**: Celery Beat (برای scheduled tasks)
|
||||
```bash
|
||||
celery -A src.infrastructure.tasks.celery_app beat -l info
|
||||
```
|
||||
|
||||
**Terminal 3**: Flower (Celery monitoring)
|
||||
```bash
|
||||
celery -A src.infrastructure.tasks.celery_app flower
|
||||
# دسترسی: http://localhost:5555
|
||||
```
|
||||
|
||||
### دسترسی به Application
|
||||
|
||||
- **Frontend**: http://localhost:3000
|
||||
- **Backend API**: http://localhost:8000
|
||||
- **Flower (Celery)**: http://localhost:5555
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Branch جدید
|
||||
|
||||
```bash
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
|
||||
### 2. کدنویسی
|
||||
|
||||
```bash
|
||||
# قبل از commit
|
||||
black src/
|
||||
isort src/
|
||||
flake8 src/
|
||||
mypy src/
|
||||
|
||||
# یا با pre-commit
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
### 3. Testing
|
||||
|
||||
```bash
|
||||
# همه تستها
|
||||
pytest
|
||||
|
||||
# با coverage
|
||||
pytest --cov=src tests/
|
||||
|
||||
# تستهای خاص
|
||||
pytest tests/unit/
|
||||
pytest tests/integration/
|
||||
|
||||
# تست یک فایل
|
||||
pytest tests/unit/test_user.py
|
||||
|
||||
# تست یک function
|
||||
pytest tests/unit/test_user.py::test_create_user
|
||||
```
|
||||
|
||||
### 4. Commit
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat(scope): description"
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
|
||||
### 5. Pull Request
|
||||
|
||||
1. Create PR از feature branch به develop
|
||||
2. منتظر code review باشید
|
||||
3. اصلاحات لازم را انجام دهید
|
||||
4. Merge
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### مشکل: ModuleNotFoundError
|
||||
|
||||
```bash
|
||||
# مطمئن شوید که در virtual environment هستید
|
||||
which python
|
||||
# باید به venv اشاره کند
|
||||
|
||||
# نصب مجدد
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### مشکل: Database Connection
|
||||
|
||||
```bash
|
||||
# بررسی PostgreSQL
|
||||
sudo systemctl status postgresql
|
||||
|
||||
# شروع PostgreSQL
|
||||
sudo systemctl start postgresql
|
||||
|
||||
# تست connection
|
||||
psql -U username -d peikarband
|
||||
```
|
||||
|
||||
### مشکل: Redis Connection
|
||||
|
||||
```bash
|
||||
# بررسی Redis
|
||||
redis-cli ping
|
||||
|
||||
# شروع Redis
|
||||
redis-server
|
||||
|
||||
# بررسی port
|
||||
netstat -an | grep 6379
|
||||
```
|
||||
|
||||
### مشکل: Port Already in Use
|
||||
|
||||
```bash
|
||||
# پیدا کردن process
|
||||
lsof -i :3000
|
||||
lsof -i :8000
|
||||
|
||||
# Kill process
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
## ابزارهای مفید
|
||||
|
||||
### IDE Setup
|
||||
|
||||
**VS Code Extensions**:
|
||||
- Python
|
||||
- Pylance
|
||||
- Python Test Explorer
|
||||
- GitLens
|
||||
- Better Comments
|
||||
|
||||
**PyCharm**: تنظیمات پیشنهادی در `.idea/` موجود است.
|
||||
|
||||
### Database Tools
|
||||
|
||||
- **pgAdmin**: GUI برای PostgreSQL
|
||||
- **DBeaver**: Multi-database tool
|
||||
- **TablePlus**: Modern database GUI
|
||||
|
||||
### API Testing
|
||||
|
||||
- **httpie**: `pip install httpie`
|
||||
- **Postman**: Desktop app
|
||||
- **Insomnia**: Alternative to Postman
|
||||
|
||||
## بروزرسانی Dependencies
|
||||
|
||||
```bash
|
||||
# بررسی dependencies قدیمی
|
||||
pip list --outdated
|
||||
|
||||
# بروزرسانی یک package
|
||||
pip install --upgrade package-name
|
||||
|
||||
# بروزرسانی همه (با دقت!)
|
||||
pip install --upgrade -r requirements.txt
|
||||
|
||||
# ذخیره نسخههای جدید
|
||||
pip freeze > requirements.txt
|
||||
```
|
||||
|
||||
## مشکلات رایج
|
||||
|
||||
### Import Error
|
||||
|
||||
اطمینان حاصل کنید که:
|
||||
- Virtual environment فعال است
|
||||
- `PYTHONPATH` صحیح است
|
||||
- همه `__init__.py` ها موجودند
|
||||
|
||||
### Alembic Migration Error
|
||||
|
||||
```bash
|
||||
# Reset alembic
|
||||
alembic downgrade base
|
||||
alembic upgrade head
|
||||
|
||||
# ایجاد migration جدید
|
||||
alembic revision --autogenerate -m "description"
|
||||
```
|
||||
|
||||
### Test Failures
|
||||
|
||||
```bash
|
||||
# پاک کردن cache
|
||||
pytest --cache-clear
|
||||
|
||||
# اجرا با verbose
|
||||
pytest -vv
|
||||
|
||||
# اجرا با output کامل
|
||||
pytest -s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**نکته**: این راهنما همیشه بهروز نگه داشته میشود. در صورت مشکل به team lead مراجعه کنید.
|
||||
|
||||
902
docs/handbook.md
Normal file
902
docs/handbook.md
Normal file
@@ -0,0 +1,902 @@
|
||||
# پروژه پیکربند - Handbook جامع
|
||||
|
||||
**نسخه**: 1.0.0
|
||||
**آخرین بروزرسانی**: 2025-01-24
|
||||
**نویسنده**: تیم توسعه پیکربند
|
||||
|
||||
---
|
||||
|
||||
## فهرست مطالب
|
||||
|
||||
1. [معرفی پروژه](#1-معرفی-پروژه)
|
||||
2. [معماری کلی](#2-معماری-کلی)
|
||||
3. [قوانین و استانداردها](#3-قوانین-و-استانداردها)
|
||||
4. [راهاندازی محیط توسعه](#4-راهاندازی-محیط-توسعه)
|
||||
5. [ساختار پروژه](#5-ساختار-پروژه)
|
||||
6. [Design Patterns](#6-design-patterns)
|
||||
7. [Best Practices](#7-best-practices)
|
||||
8. [Testing Strategy](#8-testing-strategy)
|
||||
9. [Deployment Guide](#9-deployment-guide)
|
||||
10. [Troubleshooting](#10-troubleshooting)
|
||||
11. [مستندسازی تغییرات](#11-مستندسازی-تغییرات)
|
||||
|
||||
---
|
||||
|
||||
## 1. معرفی پروژه
|
||||
|
||||
### 1.1 هدف
|
||||
|
||||
پیکربند یک پلتفرم جامع مدیریت هاستینگ و زیرساخت ابری است که شامل:
|
||||
|
||||
- **هاستینگ وردپرس**: مدیریت حرفهای سایتهای WordPress
|
||||
- **فروش دامین**: ثبت و مدیریت دامینها
|
||||
- **سرورهای اختصاصی**: VPS و Dedicated Servers
|
||||
- **خدمات DevOps**: مشاوره و پیادهسازی
|
||||
- **پنل مدیریت**: پنل کنترل سفارشی شبیه cPanel
|
||||
|
||||
### 1.2 تکنولوژیهای استفاده شده
|
||||
|
||||
| تکنولوژی | نسخه | کاربرد |
|
||||
|----------|------|--------|
|
||||
| Python | 3.11+ | زبان اصلی |
|
||||
| Reflex | 0.4.0 | Frontend/Backend Framework |
|
||||
| PostgreSQL | 14+ | Database اصلی |
|
||||
| Redis | 7+ | Cache & Sessions |
|
||||
| SQLAlchemy | 2.0+ | ORM |
|
||||
| Celery | 5.3+ | Task Queue |
|
||||
| pytest | 7.4+ | Testing |
|
||||
|
||||
### 1.3 اصول طراحی
|
||||
|
||||
#### Clean Architecture
|
||||
پروژه بر اساس معماری تمیز طراحی شده که شامل 4 لایه اصلی است:
|
||||
|
||||
1. **Domain Layer**: منطق کسبوکار خالص
|
||||
2. **Application Layer**: موارد استفاده (Use Cases)
|
||||
3. **Infrastructure Layer**: جزئیات فنی
|
||||
4. **Presentation Layer**: رابط کاربری
|
||||
|
||||
#### SOLID Principles
|
||||
- **S**ingle Responsibility Principle
|
||||
- **O**pen/Closed Principle
|
||||
- **L**iskov Substitution Principle
|
||||
- **I**nterface Segregation Principle
|
||||
- **D**ependency Inversion Principle
|
||||
|
||||
#### سایر اصول
|
||||
- **DRY**: Don't Repeat Yourself
|
||||
- **KISS**: Keep It Simple, Stupid
|
||||
- **YAGNI**: You Aren't Gonna Need It
|
||||
- **Convention over Configuration**
|
||||
|
||||
---
|
||||
|
||||
## 2. معماری کلی
|
||||
|
||||
### 2.1 نمای کلی لایهها
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Presentation Layer (Reflex UI) │
|
||||
│ ┌────────┐ ┌────────┐ ┌────────┐ │
|
||||
│ │Landing │ │Dashboard│ │ Admin │ │
|
||||
│ └────────┘ └────────┘ └────────┘ │
|
||||
└──────────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────────┴──────────────────────┐
|
||||
│ Application Layer (Use Cases) │
|
||||
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
|
||||
│ │ Auth │ │Billing│ │Server│ │Ticket│ │
|
||||
│ └──────┘ └──────┘ └──────┘ └──────┘ │
|
||||
└──────────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────────┴──────────────────────┐
|
||||
│ Domain Layer (Business Logic) │
|
||||
│ ┌────────┐ ┌────────┐ ┌────────┐ │
|
||||
│ │Entities│ │ Values │ │Services│ │
|
||||
│ └────────┘ └────────┘ └────────┘ │
|
||||
└──────────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────────┴──────────────────────┐
|
||||
│ Infrastructure Layer (Technical) │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ DB │ │Cache│ │ API │ │Tasks│ │
|
||||
│ └─────┘ └─────┘ └─────┘ └─────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 جریان داده
|
||||
|
||||
```
|
||||
User Request → Presentation → Application → Domain
|
||||
↓
|
||||
Infrastructure ← Application ← Domain Logic
|
||||
↓
|
||||
External APIs
|
||||
```
|
||||
|
||||
### 2.3 Layer های جزئی
|
||||
|
||||
#### Domain Layer
|
||||
- **Entities**: موجودیتهای اصلی (User, Service, Invoice, etc.)
|
||||
- **Value Objects**: اشیاء ارزشی (Email, Money, Phone)
|
||||
- **Domain Services**: منطق پیچیده دامین
|
||||
- **Enums**: مقادیر ثابت
|
||||
- **Exceptions**: خطاهای دامین
|
||||
|
||||
#### Application Layer
|
||||
- **Use Cases**: موارد استفاده سیستم
|
||||
- **DTOs**: Data Transfer Objects
|
||||
- **Interfaces**: تعریف رابطهای سرویسها
|
||||
- **Validators**: اعتبارسنجی ورودیها
|
||||
|
||||
#### Infrastructure Layer
|
||||
- **Database**: Models و Repositories
|
||||
- **Cache**: Redis implementation
|
||||
- **External**: API های خارجی
|
||||
- **Tasks**: Background jobs
|
||||
- **Security**: Authentication & Authorization
|
||||
- **Logging**: Structured logging
|
||||
|
||||
#### Presentation Layer
|
||||
- **Web**: Reflex components و pages
|
||||
- **API**: REST endpoints (optional)
|
||||
- **State**: Reflex state management
|
||||
|
||||
---
|
||||
|
||||
## 3. قوانین و استانداردها
|
||||
|
||||
### 3.1 Python Code Style (PEP 8)
|
||||
|
||||
#### Naming Conventions
|
||||
|
||||
```python
|
||||
# Classes: PascalCase
|
||||
class UserService:
|
||||
pass
|
||||
|
||||
class PaymentGateway:
|
||||
pass
|
||||
|
||||
# Functions & Methods: snake_case
|
||||
def create_user(email: str) -> User:
|
||||
pass
|
||||
|
||||
def process_payment(amount: Decimal) -> PaymentResult:
|
||||
pass
|
||||
|
||||
# Constants: UPPER_SNAKE_CASE
|
||||
MAX_LOGIN_ATTEMPTS = 3
|
||||
DEFAULT_TIMEOUT = 30
|
||||
API_VERSION = "v1"
|
||||
|
||||
# Private methods: _leading_underscore
|
||||
def _internal_helper():
|
||||
pass
|
||||
|
||||
# Protected: __double_underscore (name mangling)
|
||||
class BaseClass:
|
||||
def __private_method(self):
|
||||
pass
|
||||
|
||||
# Variables: snake_case
|
||||
user_email = "test@example.com"
|
||||
total_amount = Decimal("100.00")
|
||||
```
|
||||
|
||||
### 3.2 Type Hints (الزامی)
|
||||
|
||||
```python
|
||||
from typing import Optional, List, Dict, Any, Union
|
||||
from decimal import Decimal
|
||||
|
||||
# Function signatures
|
||||
def get_user_by_id(user_id: int) -> Optional[User]:
|
||||
pass
|
||||
|
||||
def create_invoice(
|
||||
user_id: int,
|
||||
amount: Decimal,
|
||||
items: List[InvoiceItem]
|
||||
) -> Invoice:
|
||||
pass
|
||||
|
||||
# Return types
|
||||
def get_users(limit: int = 10) -> List[User]:
|
||||
pass
|
||||
|
||||
def find_service(service_id: int) -> Optional[Service]:
|
||||
pass
|
||||
|
||||
# Complex types
|
||||
def process_config(config: Dict[str, Any]) -> bool:
|
||||
pass
|
||||
|
||||
UserOrNone = Optional[User]
|
||||
ServiceList = List[Service]
|
||||
```
|
||||
|
||||
### 3.3 Docstrings (Google Style - الزامی)
|
||||
|
||||
```python
|
||||
def create_user(
|
||||
email: str,
|
||||
password: str,
|
||||
full_name: str,
|
||||
phone: Optional[str] = None
|
||||
) -> User:
|
||||
"""Create a new user account.
|
||||
|
||||
This function creates a new user in the system with the provided
|
||||
information. Password is automatically hashed before storage.
|
||||
|
||||
Args:
|
||||
email: User's email address (must be unique)
|
||||
password: Plain text password (will be hashed)
|
||||
full_name: User's full name
|
||||
phone: Optional phone number
|
||||
|
||||
Returns:
|
||||
User: Created user object
|
||||
|
||||
Raises:
|
||||
EmailAlreadyExistsException: If email is already registered
|
||||
InvalidEmailException: If email format is invalid
|
||||
WeakPasswordException: If password doesn't meet requirements
|
||||
|
||||
Example:
|
||||
>>> user = create_user(
|
||||
... email="test@example.com",
|
||||
... password="SecurePass123!",
|
||||
... full_name="John Doe",
|
||||
... phone="+989123456789"
|
||||
... )
|
||||
>>> print(user.id)
|
||||
1
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 3.4 Import Order (با isort)
|
||||
|
||||
```python
|
||||
# 1. Standard library imports
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional, List
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
|
||||
# 2. Third-party imports
|
||||
import redis
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from pydantic import BaseModel
|
||||
|
||||
# 3. Local application imports
|
||||
from src.config.settings import settings
|
||||
from src.core.domain.entities.user import User
|
||||
from src.infrastructure.database.repositories.user_repository import UserRepository
|
||||
```
|
||||
|
||||
### 3.5 Error Handling
|
||||
|
||||
```python
|
||||
# ✅ GOOD: Specific exceptions
|
||||
try:
|
||||
user = user_repository.get_by_id(user_id)
|
||||
if not user:
|
||||
raise UserNotFoundException(f"User {user_id} not found")
|
||||
except DatabaseException as e:
|
||||
logger.error("database_error", error=str(e), user_id=user_id)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.critical("unexpected_error", error=str(e))
|
||||
raise InfrastructureException("Internal server error") from e
|
||||
|
||||
# ❌ BAD: Generic exceptions
|
||||
try:
|
||||
# code
|
||||
except: # Never do this!
|
||||
pass
|
||||
|
||||
# ❌ BAD: Too broad
|
||||
try:
|
||||
# code
|
||||
except Exception:
|
||||
pass
|
||||
```
|
||||
|
||||
### 3.6 Logging Best Practices
|
||||
|
||||
```python
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# ✅ GOOD: Structured logging with context
|
||||
logger.info(
|
||||
"user_created",
|
||||
user_id=user.id,
|
||||
email=user.email,
|
||||
registration_source="web",
|
||||
ip_address=request.ip
|
||||
)
|
||||
|
||||
logger.error(
|
||||
"payment_failed",
|
||||
user_id=user.id,
|
||||
invoice_id=invoice.id,
|
||||
amount=str(invoice.amount),
|
||||
gateway="zarinpal",
|
||||
error_code=response.code,
|
||||
error_message=response.message
|
||||
)
|
||||
|
||||
# ❌ BAD: String concatenation
|
||||
logger.info("User " + str(user.id) + " created")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. راهاندازی محیط توسعه
|
||||
|
||||
### 4.1 پیشنیازها
|
||||
|
||||
```bash
|
||||
# Check Python version (3.11+)
|
||||
python --version
|
||||
|
||||
# Check PostgreSQL (14+)
|
||||
psql --version
|
||||
|
||||
# Check Redis (7+)
|
||||
redis-cli --version
|
||||
|
||||
# Check Node.js (18+)
|
||||
node --version
|
||||
```
|
||||
|
||||
### 4.2 نصب وابستگیها
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/yourusername/peikarband.git
|
||||
cd peikarband
|
||||
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
# Install pre-commit hooks
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
### 4.3 تنظیم Environment Variables
|
||||
|
||||
```bash
|
||||
# Copy example env file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit .env file
|
||||
nano .env
|
||||
|
||||
# Required variables:
|
||||
# - DATABASE_URL
|
||||
# - REDIS_URL
|
||||
# - SECRET_KEY
|
||||
# - JWT_SECRET_KEY
|
||||
```
|
||||
|
||||
### 4.4 راهاندازی دیتابیس
|
||||
|
||||
```bash
|
||||
# Create database
|
||||
createdb peikarband
|
||||
|
||||
# Or using psql
|
||||
psql -U postgres
|
||||
CREATE DATABASE peikarband;
|
||||
\q
|
||||
|
||||
# Run migrations
|
||||
alembic upgrade head
|
||||
|
||||
# (Optional) Seed database
|
||||
python scripts/seed_database.py
|
||||
```
|
||||
|
||||
### 4.5 اجرای پروژه
|
||||
|
||||
```bash
|
||||
# Development mode
|
||||
python -m reflex run
|
||||
|
||||
# With auto-reload
|
||||
python -m reflex run --reload
|
||||
|
||||
# Production mode
|
||||
python -m reflex run --env production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. ساختار پروژه
|
||||
|
||||
### 5.1 نمای کلی
|
||||
|
||||
```
|
||||
peikarband/
|
||||
├── docs/ # Documentation
|
||||
├── src/ # Source code
|
||||
├── tests/ # Tests
|
||||
├── scripts/ # Utility scripts
|
||||
├── .github/workflows/ # CI/CD
|
||||
└── [config files] # Configuration
|
||||
```
|
||||
|
||||
### 5.2 src/ Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── config/ # Configuration
|
||||
│ ├── settings.py # Main settings
|
||||
│ ├── database.py # DB config
|
||||
│ ├── cache.py # Redis config
|
||||
│ └── logging.py # Log config
|
||||
│
|
||||
├── core/ # Core business logic
|
||||
│ ├── domain/ # Domain layer
|
||||
│ │ ├── entities/ # Business entities
|
||||
│ │ ├── value_objects/ # Value objects
|
||||
│ │ ├── enums/ # Enumerations
|
||||
│ │ └── exceptions/ # Domain exceptions
|
||||
│ │
|
||||
│ ├── application/ # Application layer
|
||||
│ │ ├── use_cases/ # Use cases
|
||||
│ │ ├── dto/ # DTOs
|
||||
│ │ ├── interfaces/ # Service interfaces
|
||||
│ │ └── validators/ # Validators
|
||||
│ │
|
||||
│ └── utils/ # Core utilities
|
||||
│
|
||||
├── infrastructure/ # Infrastructure layer
|
||||
│ ├── database/ # Database implementation
|
||||
│ ├── cache/ # Cache implementation
|
||||
│ ├── external/ # External APIs
|
||||
│ ├── tasks/ # Background tasks
|
||||
│ ├── security/ # Security utilities
|
||||
│ └── logging/ # Logging setup
|
||||
│
|
||||
├── presentation/ # Presentation layer
|
||||
│ ├── web/ # Reflex web app
|
||||
│ │ ├── pages/ # Pages
|
||||
│ │ ├── components/ # Components
|
||||
│ │ ├── state/ # State management
|
||||
│ │ └── styles/ # Styling
|
||||
│ │
|
||||
│ └── api/ # REST API (optional)
|
||||
│
|
||||
└── shared/ # Shared code
|
||||
├── events/ # Domain events
|
||||
└── messaging/ # Event bus
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Design Patterns
|
||||
|
||||
### 6.1 Repository Pattern
|
||||
|
||||
```python
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, List
|
||||
|
||||
# Interface (in core/application/interfaces/repositories/)
|
||||
class IUserRepository(ABC):
|
||||
@abstractmethod
|
||||
def get_by_id(self, user_id: int) -> Optional[User]:
|
||||
"""Get user by ID."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_by_email(self, email: str) -> Optional[User]:
|
||||
"""Get user by email."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save(self, user: User) -> User:
|
||||
"""Save or update user."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, user_id: int) -> bool:
|
||||
"""Delete user."""
|
||||
pass
|
||||
|
||||
# Implementation (in infrastructure/database/repositories/)
|
||||
class UserRepository(IUserRepository):
|
||||
def __init__(self, session: Session):
|
||||
self._session = session
|
||||
|
||||
def get_by_id(self, user_id: int) -> Optional[User]:
|
||||
model = self._session.query(UserModel).filter_by(id=user_id).first()
|
||||
return self._to_entity(model) if model else None
|
||||
|
||||
def save(self, user: User) -> User:
|
||||
model = self._to_model(user)
|
||||
self._session.add(model)
|
||||
self._session.commit()
|
||||
self._session.refresh(model)
|
||||
return self._to_entity(model)
|
||||
```
|
||||
|
||||
### 6.2 Unit of Work Pattern
|
||||
|
||||
```python
|
||||
from contextlib import contextmanager
|
||||
|
||||
class UnitOfWork:
|
||||
"""Manages database transactions."""
|
||||
|
||||
def __init__(self, session_factory):
|
||||
self.session_factory = session_factory
|
||||
self._session = None
|
||||
|
||||
def __enter__(self):
|
||||
self._session = self.session_factory()
|
||||
self.users = UserRepository(self._session)
|
||||
self.services = ServiceRepository(self._session)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type:
|
||||
self._session.rollback()
|
||||
else:
|
||||
self._session.commit()
|
||||
self._session.close()
|
||||
|
||||
# Usage
|
||||
with UnitOfWork(SessionLocal) as uow:
|
||||
user = uow.users.get_by_id(1)
|
||||
user.email = "new@email.com"
|
||||
uow.users.save(user)
|
||||
# Automatically commits on success
|
||||
```
|
||||
|
||||
### 6.3 Factory Pattern
|
||||
|
||||
```python
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class ICloudProvider(ABC):
|
||||
@abstractmethod
|
||||
def create_server(self, config: ServerConfig) -> Server:
|
||||
pass
|
||||
|
||||
class DigitalOceanProvider(ICloudProvider):
|
||||
def create_server(self, config: ServerConfig) -> Server:
|
||||
# Implementation
|
||||
pass
|
||||
|
||||
class HetznerProvider(ICloudProvider):
|
||||
def create_server(self, config: ServerConfig) -> Server:
|
||||
# Implementation
|
||||
pass
|
||||
|
||||
class CloudProviderFactory:
|
||||
@staticmethod
|
||||
def create(provider_type: str) -> ICloudProvider:
|
||||
providers = {
|
||||
"digitalocean": DigitalOceanProvider,
|
||||
"hetzner": HetznerProvider,
|
||||
"ovh": OVHProvider,
|
||||
}
|
||||
|
||||
provider_class = providers.get(provider_type)
|
||||
if not provider_class:
|
||||
raise ValueError(f"Unknown provider: {provider_type}")
|
||||
|
||||
return provider_class()
|
||||
|
||||
# Usage
|
||||
provider = CloudProviderFactory.create("digitalocean")
|
||||
server = provider.create_server(config)
|
||||
```
|
||||
|
||||
### 6.4 Strategy Pattern
|
||||
|
||||
```python
|
||||
class PaymentStrategy(ABC):
|
||||
@abstractmethod
|
||||
def pay(self, amount: Decimal) -> PaymentResult:
|
||||
pass
|
||||
|
||||
class ZarinpalPayment(PaymentStrategy):
|
||||
def pay(self, amount: Decimal) -> PaymentResult:
|
||||
# Zarinpal implementation
|
||||
pass
|
||||
|
||||
class IdPayPayment(PaymentStrategy):
|
||||
def pay(self, amount: Decimal) -> PaymentResult:
|
||||
# IdPay implementation
|
||||
pass
|
||||
|
||||
class PaymentProcessor:
|
||||
def __init__(self, strategy: PaymentStrategy):
|
||||
self._strategy = strategy
|
||||
|
||||
def process(self, amount: Decimal) -> PaymentResult:
|
||||
return self._strategy.pay(amount)
|
||||
|
||||
# Usage
|
||||
processor = PaymentProcessor(ZarinpalPayment())
|
||||
result = processor.process(Decimal("100.00"))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Best Practices
|
||||
|
||||
### 7.1 کد تمیز
|
||||
|
||||
```python
|
||||
# ✅ GOOD: Clear and simple
|
||||
def calculate_total_price(items: List[Item]) -> Decimal:
|
||||
"""Calculate total price of items."""
|
||||
return sum(item.price * item.quantity for item in items)
|
||||
|
||||
# ❌ BAD: Too complex
|
||||
def calc(i):
|
||||
t = 0
|
||||
for x in i:
|
||||
t += x[0] * x[1]
|
||||
return t
|
||||
```
|
||||
|
||||
### 7.2 Don't Repeat Yourself (DRY)
|
||||
|
||||
```python
|
||||
# ❌ BAD: Repetition
|
||||
def get_user_name(user_id: int) -> str:
|
||||
session = SessionLocal()
|
||||
user = session.query(User).filter_by(id=user_id).first()
|
||||
session.close()
|
||||
return user.name
|
||||
|
||||
def get_user_email(user_id: int) -> str:
|
||||
session = SessionLocal()
|
||||
user = session.query(User).filter_by(id=user_id).first()
|
||||
session.close()
|
||||
return user.email
|
||||
|
||||
# ✅ GOOD: Reusable
|
||||
def get_user(user_id: int) -> User:
|
||||
with get_db_context() as session:
|
||||
return session.query(User).filter_by(id=user_id).first()
|
||||
|
||||
def get_user_name(user_id: int) -> str:
|
||||
user = get_user(user_id)
|
||||
return user.name if user else None
|
||||
```
|
||||
|
||||
### 7.3 Single Responsibility
|
||||
|
||||
```python
|
||||
# ❌ BAD: Multiple responsibilities
|
||||
class User:
|
||||
def save(self):
|
||||
# Saves to database
|
||||
pass
|
||||
|
||||
def send_email(self):
|
||||
# Sends email
|
||||
pass
|
||||
|
||||
def calculate_discount(self):
|
||||
# Business logic
|
||||
pass
|
||||
|
||||
# ✅ GOOD: Separated
|
||||
class User:
|
||||
# Just entity
|
||||
pass
|
||||
|
||||
class UserRepository:
|
||||
def save(self, user: User):
|
||||
pass
|
||||
|
||||
class EmailService:
|
||||
def send_welcome_email(self, user: User):
|
||||
pass
|
||||
|
||||
class DiscountService:
|
||||
def calculate_user_discount(self, user: User):
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Testing Strategy
|
||||
|
||||
### 8.1 Test Pyramid
|
||||
|
||||
```
|
||||
/\
|
||||
/E2E\ 10% - End-to-End Tests
|
||||
/------\
|
||||
/Integr-\ 20% - Integration Tests
|
||||
/----------\
|
||||
/ Unit \ 70% - Unit Tests
|
||||
```
|
||||
|
||||
### 8.2 Unit Test Example
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
|
||||
class TestInvoiceCalculation:
|
||||
def test_calculate_total_with_items(self):
|
||||
# Arrange
|
||||
items = [
|
||||
InvoiceItem(name="Item 1", price=Decimal("10.00"), quantity=2),
|
||||
InvoiceItem(name="Item 2", price=Decimal("5.00"), quantity=3),
|
||||
]
|
||||
invoice = Invoice(items=items)
|
||||
|
||||
# Act
|
||||
total = invoice.calculate_total()
|
||||
|
||||
# Assert
|
||||
assert total == Decimal("35.00")
|
||||
|
||||
def test_calculate_total_empty(self):
|
||||
invoice = Invoice(items=[])
|
||||
assert invoice.calculate_total() == Decimal("0.00")
|
||||
```
|
||||
|
||||
### 8.3 Integration Test Example
|
||||
|
||||
```python
|
||||
@pytest.mark.integration
|
||||
class TestUserRepository:
|
||||
def test_save_and_retrieve_user(self, db_session):
|
||||
# Arrange
|
||||
repository = UserRepository(db_session)
|
||||
user = User(email="test@example.com", name="Test User")
|
||||
|
||||
# Act
|
||||
saved_user = repository.save(user)
|
||||
retrieved_user = repository.get_by_id(saved_user.id)
|
||||
|
||||
# Assert
|
||||
assert retrieved_user is not None
|
||||
assert retrieved_user.email == "test@example.com"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Deployment Guide
|
||||
|
||||
### 9.1 Production Checklist
|
||||
|
||||
- [ ] همه تستها pass شدند
|
||||
- [ ] Code coverage بالای 80%
|
||||
- [ ] Security audit انجام شد
|
||||
- [ ] Environment variables set شدند
|
||||
- [ ] Database migrations اجرا شدند
|
||||
- [ ] Monitoring setup شد
|
||||
- [ ] Backup strategy تعریف شد
|
||||
- [ ] SSL certificates نصب شدند
|
||||
- [ ] Firewall rules تنظیم شدند
|
||||
|
||||
---
|
||||
|
||||
## 10. Troubleshooting
|
||||
|
||||
### 10.1 مشکلات رایج
|
||||
|
||||
#### Database Connection Error
|
||||
|
||||
**Problem**: `sqlalchemy.exc.OperationalError`
|
||||
|
||||
**Solution**:
|
||||
1. Check DATABASE_URL in .env
|
||||
2. Verify PostgreSQL is running
|
||||
3. Check firewall rules
|
||||
4. Test connection: `psql $DATABASE_URL`
|
||||
|
||||
#### Redis Connection Error
|
||||
|
||||
**Problem**: `redis.exceptions.ConnectionError`
|
||||
|
||||
**Solution**:
|
||||
1. Check REDIS_URL in .env
|
||||
2. Verify Redis is running: `redis-cli ping`
|
||||
3. Check Redis password if set
|
||||
|
||||
---
|
||||
|
||||
## 11. مستندسازی تغییرات
|
||||
|
||||
### 11.1 Commit Message Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
**Types**:
|
||||
- feat: ویژگی جدید
|
||||
- fix: رفع باگ
|
||||
- docs: تغییرات مستندات
|
||||
- style: فرمتبندی
|
||||
- refactor: بازنویسی
|
||||
- test: اضافه کردن تست
|
||||
- chore: کارهای نگهداری
|
||||
|
||||
**Example**:
|
||||
```
|
||||
feat(auth): add two-factor authentication
|
||||
|
||||
- Implement TOTP-based 2FA
|
||||
- Add QR code generation for authenticator apps
|
||||
- Add backup codes feature
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
### 11.2 CHANGELOG.md
|
||||
|
||||
تمام تغییرات باید در CHANGELOG.md ثبت شود:
|
||||
|
||||
```markdown
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.0] - 2025-01-24
|
||||
|
||||
### Added
|
||||
- User authentication system
|
||||
- Two-factor authentication
|
||||
- Password reset functionality
|
||||
|
||||
### Changed
|
||||
- Improved database schema
|
||||
- Updated UI components
|
||||
|
||||
### Fixed
|
||||
- Payment gateway timeout issue
|
||||
- Email verification bug
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## پیوستها
|
||||
|
||||
### A. فایلهای مهم
|
||||
|
||||
- `docs/architecture/`: معماری سیستم
|
||||
- `docs/development/`: راهنمای توسعه
|
||||
- `docs/changelog/`: تاریخچه تغییرات
|
||||
- `docs/api/`: مستندات API
|
||||
|
||||
### B. لینکهای مفید
|
||||
|
||||
- [Python PEP 8](https://pep8.org/)
|
||||
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
- [SOLID Principles](https://en.wikipedia.org/wiki/SOLID)
|
||||
|
||||
---
|
||||
|
||||
**این handbook باید همیشه بهروز نگه داشته شود.**
|
||||
|
||||
24
helm/.helmignore
Normal file
24
helm/.helmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
|
||||
24
helm/peikarband/.helmignore
Normal file
24
helm/peikarband/.helmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
|
||||
18
helm/peikarband/Chart.yaml
Normal file
18
helm/peikarband/Chart.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
apiVersion: v2
|
||||
name: peikarband
|
||||
description: Peikarband Cloud Services Platform - Landing Page
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
keywords:
|
||||
- cloud
|
||||
- hosting
|
||||
- devops
|
||||
- wordpress
|
||||
maintainers:
|
||||
- name: Peikarband Team
|
||||
email: support@peikarband.ir
|
||||
sources:
|
||||
- https://github.com/peikarband/landing
|
||||
icon: https://peikarband.ir/logo.png
|
||||
|
||||
164
helm/peikarband/README.md
Normal file
164
helm/peikarband/README.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Peikarband Helm Chart
|
||||
|
||||
Official Helm chart برای deploy کردن Peikarband Cloud Platform.
|
||||
|
||||
## نصب
|
||||
|
||||
### اضافه کردن Repository
|
||||
|
||||
```bash
|
||||
helm repo add peikarband https://peikarband.github.io/charts
|
||||
helm repo update
|
||||
```
|
||||
|
||||
### نصب Chart
|
||||
|
||||
```bash
|
||||
helm install peikarband peikarband/peikarband \
|
||||
--namespace production \
|
||||
--create-namespace \
|
||||
--set image.tag=0.1.0
|
||||
```
|
||||
|
||||
## پیکربندی
|
||||
|
||||
### مهمترین Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `replicaCount` | int | `2` | تعداد replicas |
|
||||
| `image.repository` | string | `registry.example.com/peikarband/landing` | Docker image repository |
|
||||
| `image.tag` | string | `latest` | Image tag |
|
||||
| `image.pullPolicy` | string | `IfNotPresent` | Image pull policy |
|
||||
| `resources.limits.cpu` | string | `1000m` | CPU limit |
|
||||
| `resources.limits.memory` | string | `1Gi` | Memory limit |
|
||||
| `autoscaling.enabled` | bool | `true` | فعال کردن HPA |
|
||||
| `autoscaling.minReplicas` | int | `2` | حداقل replicas |
|
||||
| `autoscaling.maxReplicas` | int | `10` | حداکثر replicas |
|
||||
| `ingress.enabled` | bool | `true` | فعال کردن Ingress |
|
||||
| `ingress.hosts[0].host` | string | `peikarband.ir` | Domain |
|
||||
|
||||
### مثالهای استفاده
|
||||
|
||||
#### نصب با مقادیر سفارشی
|
||||
|
||||
```bash
|
||||
helm install peikarband peikarband/peikarband \
|
||||
--set image.tag=0.2.0 \
|
||||
--set replicaCount=3 \
|
||||
--set resources.limits.cpu=2000m \
|
||||
--set ingress.hosts[0].host=example.com
|
||||
```
|
||||
|
||||
#### استفاده از values file
|
||||
|
||||
```bash
|
||||
helm install peikarband peikarband/peikarband \
|
||||
-f my-values.yaml
|
||||
```
|
||||
|
||||
#### Upgrade
|
||||
|
||||
```bash
|
||||
helm upgrade peikarband peikarband/peikarband \
|
||||
--set image.tag=0.3.0 \
|
||||
--reuse-values
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Kubernetes 1.24+
|
||||
- Helm 3.10+
|
||||
- PostgreSQL (external یا in-cluster)
|
||||
- Redis (external یا in-cluster)
|
||||
|
||||
## Values فایلها
|
||||
|
||||
این chart شامل چند values file است:
|
||||
|
||||
- `values.yaml` - مقادیر پیشفرض
|
||||
- `values-production.yaml` - تنظیمات production
|
||||
|
||||
## Components
|
||||
|
||||
این Chart شامل موارد زیر است:
|
||||
|
||||
- **Deployment**: اجرای application
|
||||
- **Service**: ClusterIP service برای internal access
|
||||
- **Ingress**: External access با TLS
|
||||
- **ConfigMap**: تنظیمات application
|
||||
- **ServiceAccount**: Kubernetes service account
|
||||
- **HPA**: Horizontal Pod Autoscaler
|
||||
- **PDB**: Pod Disruption Budget
|
||||
- **NetworkPolicy**: محدودیتهای network
|
||||
|
||||
## پیشنیازها
|
||||
|
||||
### ساخت Secrets
|
||||
|
||||
```bash
|
||||
kubectl create secret generic peikarband-secrets \
|
||||
--from-literal=db-username=USERNAME \
|
||||
--from-literal=db-password=PASSWORD \
|
||||
--from-literal=redis-password=REDIS_PASS \
|
||||
-n production
|
||||
```
|
||||
|
||||
### cert-manager (برای TLS)
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### مشاهده وضعیت
|
||||
|
||||
```bash
|
||||
helm status peikarband -n production
|
||||
kubectl get all -n production
|
||||
kubectl get pods -l app.kubernetes.io/name=peikarband -n production
|
||||
```
|
||||
|
||||
### مشاهده Logs
|
||||
|
||||
```bash
|
||||
kubectl logs -f deployment/peikarband -n production
|
||||
```
|
||||
|
||||
### Rollback
|
||||
|
||||
```bash
|
||||
helm rollback peikarband -n production
|
||||
```
|
||||
|
||||
## توسعه
|
||||
|
||||
### Lint
|
||||
|
||||
```bash
|
||||
helm lint .
|
||||
```
|
||||
|
||||
### Template
|
||||
|
||||
```bash
|
||||
helm template peikarband . --debug
|
||||
```
|
||||
|
||||
### Package
|
||||
|
||||
```bash
|
||||
helm package .
|
||||
```
|
||||
|
||||
## لایسنس
|
||||
|
||||
MIT
|
||||
|
||||
## پشتیبانی
|
||||
|
||||
- Email: support@peikarband.ir
|
||||
- Website: https://peikarband.ir
|
||||
- Docs: https://docs.peikarband.ir
|
||||
|
||||
43
helm/peikarband/templates/NOTES.txt
Normal file
43
helm/peikarband/templates/NOTES.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
Thank you for installing {{ .Chart.Name }}.
|
||||
|
||||
Your release is named {{ .Release.Name }}.
|
||||
|
||||
To learn more about the release, try:
|
||||
|
||||
$ helm status {{ .Release.Name }}
|
||||
$ helm get all {{ .Release.Name }}
|
||||
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "peikarband.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "peikarband.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "peikarband.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.frontend.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "peikarband.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
|
||||
2. Check the deployment status:
|
||||
kubectl get deployment {{ include "peikarband.fullname" . }} -n {{ .Release.Namespace }}
|
||||
|
||||
3. View logs:
|
||||
kubectl logs -f deployment/{{ include "peikarband.fullname" . }} -n {{ .Release.Namespace }}
|
||||
|
||||
4. Scale the deployment:
|
||||
kubectl scale deployment {{ include "peikarband.fullname" . }} --replicas=3 -n {{ .Release.Namespace }}
|
||||
|
||||
Peikarband - Cloud Services Platform
|
||||
|
||||
61
helm/peikarband/templates/_helpers.tpl
Normal file
61
helm/peikarband/templates/_helpers.tpl
Normal file
@@ -0,0 +1,61 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "peikarband.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "peikarband.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "peikarband.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "peikarband.labels" -}}
|
||||
helm.sh/chart: {{ include "peikarband.chart" . }}
|
||||
{{ include "peikarband.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "peikarband.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "peikarband.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "peikarband.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "peikarband.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
9
helm/peikarband/templates/configmap.yaml
Normal file
9
helm/peikarband/templates/configmap.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "peikarband.fullname" . }}
|
||||
labels:
|
||||
{{- include "peikarband.labels" . | nindent 4 }}
|
||||
data:
|
||||
{{- toYaml .Values.configMap.data | nindent 2 }}
|
||||
|
||||
99
helm/peikarband/templates/deployment.yaml
Normal file
99
helm/peikarband/templates/deployment.yaml
Normal file
@@ -0,0 +1,99 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "peikarband.fullname" . }}
|
||||
labels:
|
||||
{{- include "peikarband.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "peikarband.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "peikarband.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "peikarband.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: backend
|
||||
containerPort: {{ .Values.service.backend.targetPort }}
|
||||
protocol: TCP
|
||||
- name: frontend
|
||||
containerPort: {{ .Values.service.frontend.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- toYaml .Values.env | nindent 12 }}
|
||||
{{- if .Values.postgresql.enabled }}
|
||||
- name: DATABASE_HOST
|
||||
value: {{ .Values.postgresql.external.host }}
|
||||
- name: DATABASE_PORT
|
||||
value: {{ .Values.postgresql.external.port | quote }}
|
||||
- name: DATABASE_NAME
|
||||
value: {{ .Values.postgresql.external.database }}
|
||||
- name: DATABASE_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.postgresql.external.usernameSecret.name }}
|
||||
key: {{ .Values.postgresql.external.usernameSecret.key }}
|
||||
- name: DATABASE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.postgresql.external.passwordSecret.name }}
|
||||
key: {{ .Values.postgresql.external.passwordSecret.key }}
|
||||
{{- end }}
|
||||
{{- if .Values.redis.enabled }}
|
||||
- name: REDIS_HOST
|
||||
value: {{ .Values.redis.external.host }}
|
||||
- name: REDIS_PORT
|
||||
value: {{ .Values.redis.external.port | quote }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.redis.external.passwordSecret.name }}
|
||||
key: {{ .Values.redis.external.passwordSecret.key }}
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "peikarband.fullname" . }}
|
||||
{{- with .Values.envFrom }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
33
helm/peikarband/templates/hpa.yaml
Normal file
33
helm/peikarband/templates/hpa.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "peikarband.fullname" . }}
|
||||
labels:
|
||||
{{- include "peikarband.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "peikarband.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
42
helm/peikarband/templates/ingress.yaml
Normal file
42
helm/peikarband/templates/ingress.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "peikarband.fullname" . }}
|
||||
labels:
|
||||
{{- include "peikarband.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.className }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "peikarband.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.frontend.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
19
helm/peikarband/templates/networkpolicy.yaml
Normal file
19
helm/peikarband/templates/networkpolicy.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
{{- if .Values.networkPolicy.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: {{ include "peikarband.fullname" . }}
|
||||
labels:
|
||||
{{- include "peikarband.labels" . | nindent 4 }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
{{- include "peikarband.selectorLabels" . | nindent 6 }}
|
||||
policyTypes:
|
||||
{{- toYaml .Values.networkPolicy.policyTypes | nindent 4 }}
|
||||
ingress:
|
||||
{{- toYaml .Values.networkPolicy.ingress | nindent 4 }}
|
||||
egress:
|
||||
{{- toYaml .Values.networkPolicy.egress | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
14
helm/peikarband/templates/pdb.yaml
Normal file
14
helm/peikarband/templates/pdb.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
{{- if .Values.podDisruptionBudget.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "peikarband.fullname" . }}
|
||||
labels:
|
||||
{{- include "peikarband.labels" . | nindent 4 }}
|
||||
spec:
|
||||
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "peikarband.selectorLabels" . | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
20
helm/peikarband/templates/service.yaml
Normal file
20
helm/peikarband/templates/service.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "peikarband.fullname" . }}
|
||||
labels:
|
||||
{{- include "peikarband.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.backend.port }}
|
||||
targetPort: backend
|
||||
protocol: TCP
|
||||
name: backend
|
||||
- port: {{ .Values.service.frontend.port }}
|
||||
targetPort: frontend
|
||||
protocol: TCP
|
||||
name: frontend
|
||||
selector:
|
||||
{{- include "peikarband.selectorLabels" . | nindent 4 }}
|
||||
|
||||
13
helm/peikarband/templates/serviceaccount.yaml
Normal file
13
helm/peikarband/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "peikarband.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "peikarband.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
93
helm/peikarband/values-production.yaml
Normal file
93
helm/peikarband/values-production.yaml
Normal file
@@ -0,0 +1,93 @@
|
||||
# Production-specific values for peikarband
|
||||
# This file overrides default values.yaml for production
|
||||
|
||||
replicaCount: 3
|
||||
|
||||
image:
|
||||
pullPolicy: Always
|
||||
|
||||
podAnnotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8000"
|
||||
prometheus.io/path: "/metrics"
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 2Gi
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicas: 3
|
||||
maxReplicas: 20
|
||||
targetCPUUtilizationPercentage: 60
|
||||
targetMemoryUtilizationPercentage: 70
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/rate-limit: "100"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
|
||||
hosts:
|
||||
- host: peikarband.ir
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
- host: www.peikarband.ir
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: peikarband-tls
|
||||
hosts:
|
||||
- peikarband.ir
|
||||
- www.peikarband.ir
|
||||
|
||||
postgresql:
|
||||
enabled: true
|
||||
external:
|
||||
host: "postgres-prod.default.svc.cluster.local"
|
||||
port: "5432"
|
||||
database: "peikarband_prod"
|
||||
usernameSecret:
|
||||
name: "peikarband-prod-secrets"
|
||||
key: "db-username"
|
||||
passwordSecret:
|
||||
name: "peikarband-prod-secrets"
|
||||
key: "db-password"
|
||||
|
||||
redis:
|
||||
enabled: true
|
||||
external:
|
||||
host: "redis-prod.default.svc.cluster.local"
|
||||
port: "6379"
|
||||
passwordSecret:
|
||||
name: "peikarband-prod-secrets"
|
||||
key: "redis-password"
|
||||
|
||||
configMap:
|
||||
data:
|
||||
APP_NAME: "peikarband"
|
||||
LOG_LEVEL: "warning"
|
||||
ENVIRONMENT: "production"
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
minAvailable: 2
|
||||
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
|
||||
monitoring:
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
|
||||
63
helm/peikarband/values-staging.yaml
Normal file
63
helm/peikarband/values-staging.yaml
Normal file
@@ -0,0 +1,63 @@
|
||||
# Staging-specific values for peikarband
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
pullPolicy: Always
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-staging"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
hosts:
|
||||
- host: staging.peikarband.ir
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: peikarband-staging-tls
|
||||
hosts:
|
||||
- staging.peikarband.ir
|
||||
|
||||
postgresql:
|
||||
enabled: true
|
||||
external:
|
||||
host: "postgres-staging.default.svc.cluster.local"
|
||||
port: "5432"
|
||||
database: "peikarband_staging"
|
||||
|
||||
redis:
|
||||
enabled: true
|
||||
external:
|
||||
host: "redis-staging.default.svc.cluster.local"
|
||||
port: "6379"
|
||||
|
||||
configMap:
|
||||
data:
|
||||
APP_NAME: "peikarband-staging"
|
||||
LOG_LEVEL: "debug"
|
||||
ENVIRONMENT: "staging"
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
|
||||
networkPolicy:
|
||||
enabled: false
|
||||
|
||||
monitoring:
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
|
||||
201
helm/peikarband/values.yaml
Normal file
201
helm/peikarband/values.yaml
Normal file
@@ -0,0 +1,201 @@
|
||||
# Default values for peikarband
|
||||
# This is a YAML-formatted file.
|
||||
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: registry.example.com/peikarband/landing
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations: {}
|
||||
name: ""
|
||||
|
||||
podAnnotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8000"
|
||||
prometheus.io/path: "/metrics"
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: false
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
backend:
|
||||
port: 8000
|
||||
targetPort: 8000
|
||||
frontend:
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
hosts:
|
||||
- host: peikarband.ir
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
- host: www.peikarband.ir
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: peikarband-tls
|
||||
hosts:
|
||||
- peikarband.ir
|
||||
- www.peikarband.ir
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/name
|
||||
operator: In
|
||||
values:
|
||||
- peikarband
|
||||
topologyKey: kubernetes.io/hostname
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
|
||||
env:
|
||||
- name: REFLEX_ENV
|
||||
value: "production"
|
||||
- name: PYTHONUNBUFFERED
|
||||
value: "1"
|
||||
|
||||
envFrom: []
|
||||
|
||||
configMap:
|
||||
data:
|
||||
APP_NAME: "peikarband"
|
||||
LOG_LEVEL: "info"
|
||||
|
||||
secretRef:
|
||||
name: "peikarband-secrets"
|
||||
|
||||
postgresql:
|
||||
enabled: false
|
||||
external:
|
||||
host: "postgres.default.svc.cluster.local"
|
||||
port: "5432"
|
||||
database: "peikarband"
|
||||
usernameSecret:
|
||||
name: "peikarband-secrets"
|
||||
key: "db-username"
|
||||
passwordSecret:
|
||||
name: "peikarband-secrets"
|
||||
key: "db-password"
|
||||
|
||||
redis:
|
||||
enabled: false
|
||||
external:
|
||||
host: "redis.default.svc.cluster.local"
|
||||
port: "6379"
|
||||
passwordSecret:
|
||||
name: "peikarband-secrets"
|
||||
key: "redis-password"
|
||||
|
||||
persistence:
|
||||
enabled: false
|
||||
storageClass: ""
|
||||
accessMode: ReadWriteOnce
|
||||
size: 10Gi
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
minAvailable: 1
|
||||
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: ingress-nginx
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8000
|
||||
- protocol: TCP
|
||||
port: 3000
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432 # PostgreSQL
|
||||
- protocol: TCP
|
||||
port: 6379 # Redis
|
||||
- protocol: TCP
|
||||
port: 443 # HTTPS
|
||||
- protocol: TCP
|
||||
port: 80 # HTTP
|
||||
- protocol: UDP
|
||||
port: 53 # DNS
|
||||
|
||||
monitoring:
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
|
||||
30
mypy.ini
Normal file
30
mypy.ini
Normal file
@@ -0,0 +1,30 @@
|
||||
[mypy]
|
||||
# Mypy configuration for type checking
|
||||
|
||||
python_version = 3.11
|
||||
warn_return_any = True
|
||||
warn_unused_configs = True
|
||||
disallow_untyped_defs = True
|
||||
disallow_any_unimported = False
|
||||
no_implicit_optional = True
|
||||
warn_redundant_casts = True
|
||||
warn_unused_ignores = True
|
||||
warn_no_return = True
|
||||
warn_unreachable = True
|
||||
strict_equality = True
|
||||
|
||||
[mypy-tests.*]
|
||||
disallow_untyped_defs = False
|
||||
|
||||
[mypy-reflex.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-sqlalchemy.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-redis.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-celery.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
6
peikarband/__init__.py
Normal file
6
peikarband/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Peikarband application package."""
|
||||
|
||||
from .peikarband import app
|
||||
|
||||
__all__ = ["app"]
|
||||
|
||||
10
peikarband/peikarband.py
Normal file
10
peikarband/peikarband.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Peikarband main application entry point."""
|
||||
|
||||
import reflex as rx
|
||||
|
||||
# Import landing page
|
||||
from src.presentation.web.pages.landing.index import app as landing_app
|
||||
|
||||
# Create main app
|
||||
app = landing_app
|
||||
|
||||
43
pytest.ini
Normal file
43
pytest.ini
Normal file
@@ -0,0 +1,43 @@
|
||||
[pytest]
|
||||
# Pytest configuration for Peikarband
|
||||
|
||||
# Test discovery
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
|
||||
# Output
|
||||
addopts =
|
||||
-v
|
||||
--strict-markers
|
||||
--tb=short
|
||||
--cov=src
|
||||
--cov-report=html
|
||||
--cov-report=term-missing
|
||||
--cov-fail-under=80
|
||||
|
||||
# Markers
|
||||
markers =
|
||||
unit: Unit tests
|
||||
integration: Integration tests
|
||||
e2e: End-to-end tests
|
||||
slow: Slow tests
|
||||
fast: Fast tests
|
||||
database: Tests that require database
|
||||
redis: Tests that require Redis
|
||||
external: Tests that call external APIs
|
||||
|
||||
# Coverage
|
||||
[coverage:run]
|
||||
source = src
|
||||
omit =
|
||||
*/tests/*
|
||||
*/migrations/*
|
||||
*/__init__.py
|
||||
|
||||
[coverage:report]
|
||||
precision = 2
|
||||
show_missing = True
|
||||
skip_covered = False
|
||||
|
||||
58
requirements-dev.txt
Normal file
58
requirements-dev.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
# Peikarband Platform - Development Dependencies
|
||||
|
||||
# Include core requirements
|
||||
-r requirements.txt
|
||||
|
||||
# ============================================
|
||||
# Testing
|
||||
# ============================================
|
||||
pytest==7.4.3
|
||||
pytest-cov==4.1.0
|
||||
pytest-asyncio==0.21.1
|
||||
pytest-mock==3.12.0
|
||||
pytest-xdist==3.5.0
|
||||
factory-boy==3.3.0
|
||||
faker==20.1.0
|
||||
|
||||
# ============================================
|
||||
# Code Quality
|
||||
# ============================================
|
||||
black==23.12.0
|
||||
flake8==6.1.0
|
||||
isort==5.13.2
|
||||
mypy==1.7.1
|
||||
pylint==3.0.3
|
||||
autopep8==2.0.4
|
||||
|
||||
# ============================================
|
||||
# Type Stubs
|
||||
# ============================================
|
||||
types-redis==4.6.0.11
|
||||
types-requests==2.31.0.10
|
||||
types-python-dateutil==2.8.19.14
|
||||
|
||||
# ============================================
|
||||
# Pre-commit Hooks
|
||||
# ============================================
|
||||
pre-commit==3.5.0
|
||||
|
||||
# ============================================
|
||||
# Documentation
|
||||
# ============================================
|
||||
mkdocs==1.5.3
|
||||
mkdocs-material==9.5.2
|
||||
mkdocstrings[python]==0.24.0
|
||||
|
||||
# ============================================
|
||||
# Development Tools
|
||||
# ============================================
|
||||
ipython==8.18.1
|
||||
ipdb==0.13.13
|
||||
rich==13.7.0
|
||||
|
||||
# ============================================
|
||||
# Security Scanning
|
||||
# ============================================
|
||||
bandit==1.7.5
|
||||
safety==2.3.5
|
||||
|
||||
90
requirements.txt
Normal file
90
requirements.txt
Normal file
@@ -0,0 +1,90 @@
|
||||
# Peikarband Platform - Core Dependencies
|
||||
|
||||
# ============================================
|
||||
# Core Framework
|
||||
# ============================================
|
||||
reflex==0.4.0
|
||||
|
||||
# ============================================
|
||||
# Database & ORM
|
||||
# ============================================
|
||||
sqlalchemy==2.0.23
|
||||
psycopg2-binary==2.9.9
|
||||
alembic==1.13.0
|
||||
|
||||
# ============================================
|
||||
# Data Validation
|
||||
# ============================================
|
||||
pydantic==2.5.2
|
||||
pydantic-settings==2.1.0
|
||||
email-validator==2.1.0
|
||||
|
||||
# ============================================
|
||||
# Caching
|
||||
# ============================================
|
||||
redis==5.0.1
|
||||
|
||||
# ============================================
|
||||
# Task Queue
|
||||
# ============================================
|
||||
celery==5.3.4
|
||||
flower==2.0.1
|
||||
|
||||
# ============================================
|
||||
# Security & Authentication
|
||||
# ============================================
|
||||
passlib[bcrypt]==1.7.4
|
||||
pyjwt==2.8.0
|
||||
pyotp==2.9.0
|
||||
cryptography==41.0.7
|
||||
|
||||
# ============================================
|
||||
# Cloud Provider APIs
|
||||
# ============================================
|
||||
python-digitalocean==1.17.0
|
||||
hcloud==1.33.2
|
||||
python-ovh==1.1.0
|
||||
|
||||
# ============================================
|
||||
# Payment Gateways
|
||||
# ============================================
|
||||
zarinpal==1.0.0
|
||||
idpay==1.0.0
|
||||
|
||||
# ============================================
|
||||
# HTTP Client
|
||||
# ============================================
|
||||
httpx==0.25.2
|
||||
requests==2.31.0
|
||||
|
||||
# ============================================
|
||||
# Logging
|
||||
# ============================================
|
||||
structlog==23.2.0
|
||||
python-json-logger==2.0.7
|
||||
|
||||
# ============================================
|
||||
# Monitoring & Error Tracking
|
||||
# ============================================
|
||||
sentry-sdk==1.38.0
|
||||
prometheus-client==0.19.0
|
||||
|
||||
# ============================================
|
||||
# Utilities
|
||||
# ============================================
|
||||
python-decouple==3.8
|
||||
python-dotenv==1.0.0
|
||||
tenacity==8.2.3
|
||||
python-multipart==0.0.6
|
||||
|
||||
# ============================================
|
||||
# Server Management
|
||||
# ============================================
|
||||
paramiko==3.4.0
|
||||
fabric==3.2.2
|
||||
|
||||
# ============================================
|
||||
# Date & Time
|
||||
# ============================================
|
||||
python-dateutil==2.8.2
|
||||
pytz==2023.3
|
||||
19
rxconfig.py
Normal file
19
rxconfig.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Reflex configuration file.
|
||||
|
||||
This file configures the Reflex application settings.
|
||||
"""
|
||||
|
||||
import reflex as rx
|
||||
|
||||
config = rx.Config(
|
||||
app_name="peikarband",
|
||||
api_url="http://localhost:8000",
|
||||
frontend_port=3000,
|
||||
backend_port=8000,
|
||||
db_url="sqlite:///reflex.db", # Temporary, will use PostgreSQL
|
||||
disable_plugins=["reflex.plugins.sitemap.SitemapPlugin"],
|
||||
stylesheets=[
|
||||
"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap",
|
||||
"https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css",
|
||||
],
|
||||
)
|
||||
39
setup.py
Normal file
39
setup.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Setup configuration for Peikarband platform."""
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
with open("README.md", "r", encoding="utf-8") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
with open("requirements.txt", "r", encoding="utf-8") as fh:
|
||||
requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")]
|
||||
|
||||
setup(
|
||||
name="peikarband",
|
||||
version="0.1.0",
|
||||
author="Peikarband Team",
|
||||
author_email="support@peikarband.ir",
|
||||
description="پلتفرم جامع مدیریت هاستینگ و زیرساخت ابری",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/yourusername/peikarband",
|
||||
packages=find_packages(where="src"),
|
||||
package_dir={"": "src"},
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
],
|
||||
python_requires=">=3.11",
|
||||
install_requires=requirements,
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"peikarband=src.presentation.web.app:main",
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/config/__init__.py
Normal file
0
src/config/__init__.py
Normal file
106
src/config/cache.py
Normal file
106
src/config/cache.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""Redis cache configuration and management.
|
||||
|
||||
This module handles Redis connections and caching operations.
|
||||
"""
|
||||
|
||||
from typing import Optional, Any
|
||||
from redis import Redis, ConnectionPool
|
||||
import json
|
||||
|
||||
from src.config.settings import settings
|
||||
|
||||
|
||||
# Redis Connection Pool
|
||||
redis_pool = ConnectionPool.from_url(
|
||||
settings.REDIS_URL,
|
||||
max_connections=settings.REDIS_MAX_CONNECTIONS,
|
||||
decode_responses=True,
|
||||
)
|
||||
|
||||
# Redis Client
|
||||
redis_client = Redis(connection_pool=redis_pool)
|
||||
|
||||
|
||||
def get_redis() -> Redis:
|
||||
"""Get Redis client instance.
|
||||
|
||||
Returns:
|
||||
Redis: Redis client
|
||||
"""
|
||||
return redis_client
|
||||
|
||||
|
||||
def cache_set(
|
||||
key: str,
|
||||
value: Any,
|
||||
ttl: Optional[int] = None
|
||||
) -> bool:
|
||||
"""Set cache value.
|
||||
|
||||
Args:
|
||||
key: Cache key
|
||||
value: Value to cache
|
||||
ttl: Time to live in seconds
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
try:
|
||||
serialized_value = json.dumps(value)
|
||||
if ttl:
|
||||
return redis_client.setex(key, ttl, serialized_value)
|
||||
return redis_client.set(key, serialized_value)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def cache_get(key: str) -> Optional[Any]:
|
||||
"""Get cache value.
|
||||
|
||||
Args:
|
||||
key: Cache key
|
||||
|
||||
Returns:
|
||||
Optional[Any]: Cached value or None
|
||||
"""
|
||||
try:
|
||||
value = redis_client.get(key)
|
||||
if value:
|
||||
return json.loads(value)
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def cache_delete(key: str) -> bool:
|
||||
"""Delete cache value.
|
||||
|
||||
Args:
|
||||
key: Cache key
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
try:
|
||||
return bool(redis_client.delete(key))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def cache_clear_pattern(pattern: str) -> int:
|
||||
"""Clear cache by pattern.
|
||||
|
||||
Args:
|
||||
pattern: Key pattern (e.g., "user:*")
|
||||
|
||||
Returns:
|
||||
int: Number of keys deleted
|
||||
"""
|
||||
try:
|
||||
keys = redis_client.keys(pattern)
|
||||
if keys:
|
||||
return redis_client.delete(*keys)
|
||||
return 0
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
85
src/config/database.py
Normal file
85
src/config/database.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Database configuration and connection management.
|
||||
|
||||
This module handles database connections, session management, and connection pooling.
|
||||
"""
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
|
||||
from src.config.settings import settings
|
||||
|
||||
|
||||
# SQLAlchemy Base
|
||||
Base = declarative_base()
|
||||
|
||||
# Database Engine
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
pool_size=settings.DATABASE_POOL_SIZE,
|
||||
max_overflow=settings.DATABASE_MAX_OVERFLOW,
|
||||
pool_pre_ping=True, # Enable connection health checks
|
||||
echo=settings.DEBUG, # Log SQL queries in debug mode
|
||||
)
|
||||
|
||||
# Session Factory
|
||||
SessionLocal = sessionmaker(
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine
|
||||
)
|
||||
|
||||
|
||||
def get_session() -> Generator[Session, None, None]:
|
||||
"""Get database session.
|
||||
|
||||
Yields:
|
||||
Session: SQLAlchemy session
|
||||
|
||||
Example:
|
||||
>>> with get_session() as session:
|
||||
... user = session.query(User).first()
|
||||
"""
|
||||
session = SessionLocal()
|
||||
try:
|
||||
yield session
|
||||
session.commit()
|
||||
except Exception:
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_context() -> Generator[Session, None, None]:
|
||||
"""Context manager for database session.
|
||||
|
||||
Yields:
|
||||
Session: SQLAlchemy session
|
||||
"""
|
||||
session = SessionLocal()
|
||||
try:
|
||||
yield session
|
||||
session.commit()
|
||||
except Exception:
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
"""Initialize database (create all tables).
|
||||
|
||||
Note: In production, use Alembic migrations instead.
|
||||
"""
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
def close_db() -> None:
|
||||
"""Close database connections."""
|
||||
engine.dispose()
|
||||
|
||||
0
src/config/environments/__init__.py
Normal file
0
src/config/environments/__init__.py
Normal file
56
src/config/logging.py
Normal file
56
src/config/logging.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Logging configuration using structlog.
|
||||
|
||||
This module sets up structured logging for the application.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import structlog
|
||||
from typing import Any
|
||||
|
||||
from src.config.settings import settings
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
"""Configure structured logging with structlog."""
|
||||
|
||||
# Configure structlog
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.processors.add_log_level,
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.dev.set_exc_info,
|
||||
structlog.processors.TimeStamper(fmt="iso", utc=True),
|
||||
structlog.processors.JSONRenderer() if settings.LOG_FORMAT == "json"
|
||||
else structlog.dev.ConsoleRenderer(),
|
||||
],
|
||||
wrapper_class=structlog.make_filtering_bound_logger(
|
||||
logging.getLevelName(settings.LOG_LEVEL)
|
||||
),
|
||||
context_class=dict,
|
||||
logger_factory=structlog.PrintLoggerFactory(),
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
|
||||
# Configure standard logging
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
level=logging.getLevelName(settings.LOG_LEVEL),
|
||||
)
|
||||
|
||||
|
||||
def get_logger(name: str) -> Any:
|
||||
"""Get a logger instance.
|
||||
|
||||
Args:
|
||||
name: Logger name (usually __name__)
|
||||
|
||||
Returns:
|
||||
Logger instance
|
||||
|
||||
Example:
|
||||
>>> logger = get_logger(__name__)
|
||||
>>> logger.info("user_created", user_id=123, email="test@example.com")
|
||||
"""
|
||||
return structlog.get_logger(name)
|
||||
|
||||
74
src/config/settings.py
Normal file
74
src/config/settings.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Main configuration settings for Peikarband platform.
|
||||
|
||||
This module contains all configuration settings loaded from environment variables.
|
||||
Follows PEP 8 and clean code principles.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseSettings, Field, validator
|
||||
import os
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Main settings class using Pydantic for validation."""
|
||||
|
||||
# Application
|
||||
APP_NAME: str = "Peikarband"
|
||||
APP_VERSION: str = "0.1.0"
|
||||
DEBUG: bool = Field(default=False, env="DEBUG")
|
||||
ENVIRONMENT: str = Field(default="development", env="ENVIRONMENT")
|
||||
|
||||
# Database
|
||||
DATABASE_URL: str = Field(..., env="DATABASE_URL")
|
||||
DATABASE_POOL_SIZE: int = Field(default=20, env="DATABASE_POOL_SIZE")
|
||||
DATABASE_MAX_OVERFLOW: int = Field(default=10, env="DATABASE_MAX_OVERFLOW")
|
||||
|
||||
# Redis
|
||||
REDIS_URL: str = Field(..., env="REDIS_URL")
|
||||
REDIS_MAX_CONNECTIONS: int = Field(default=50, env="REDIS_MAX_CONNECTIONS")
|
||||
|
||||
# Security
|
||||
SECRET_KEY: str = Field(..., env="SECRET_KEY")
|
||||
JWT_SECRET_KEY: str = Field(..., env="JWT_SECRET_KEY")
|
||||
JWT_ALGORITHM: str = Field(default="HS256", env="JWT_ALGORITHM")
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, env="JWT_ACCESS_TOKEN_EXPIRE_MINUTES")
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7, env="JWT_REFRESH_TOKEN_EXPIRE_DAYS")
|
||||
|
||||
# Celery
|
||||
CELERY_BROKER_URL: str = Field(..., env="CELERY_BROKER_URL")
|
||||
CELERY_RESULT_BACKEND: str = Field(..., env="CELERY_RESULT_BACKEND")
|
||||
|
||||
# External APIs
|
||||
DIGITALOCEAN_API_KEY: Optional[str] = Field(default=None, env="DIGITALOCEAN_API_KEY")
|
||||
HETZNER_API_KEY: Optional[str] = Field(default=None, env="HETZNER_API_KEY")
|
||||
OVH_API_KEY: Optional[str] = Field(default=None, env="OVH_API_KEY")
|
||||
|
||||
# Payment Gateways
|
||||
ZARINPAL_MERCHANT_ID: Optional[str] = Field(default=None, env="ZARINPAL_MERCHANT_ID")
|
||||
IDPAY_API_KEY: Optional[str] = Field(default=None, env="IDPAY_API_KEY")
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL: str = Field(default="INFO", env="LOG_LEVEL")
|
||||
LOG_FORMAT: str = Field(default="json", env="LOG_FORMAT")
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS: list[str] = Field(default=["*"], env="CORS_ORIGINS")
|
||||
|
||||
class Config:
|
||||
"""Pydantic configuration."""
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
case_sensitive = True
|
||||
|
||||
@validator("ENVIRONMENT")
|
||||
def validate_environment(cls, v: str) -> str:
|
||||
"""Validate environment value."""
|
||||
allowed = ["development", "staging", "production"]
|
||||
if v not in allowed:
|
||||
raise ValueError(f"ENVIRONMENT must be one of {allowed}")
|
||||
return v
|
||||
|
||||
|
||||
# Global settings instance
|
||||
settings = Settings()
|
||||
|
||||
0
src/core/__init__.py
Normal file
0
src/core/__init__.py
Normal file
0
src/core/application/__init__.py
Normal file
0
src/core/application/__init__.py
Normal file
0
src/core/application/dto/__init__.py
Normal file
0
src/core/application/dto/__init__.py
Normal file
0
src/core/application/interfaces/__init__.py
Normal file
0
src/core/application/interfaces/__init__.py
Normal file
0
src/core/application/use_cases/__init__.py
Normal file
0
src/core/application/use_cases/__init__.py
Normal file
0
src/core/application/use_cases/auth/__init__.py
Normal file
0
src/core/application/use_cases/auth/__init__.py
Normal file
0
src/core/application/use_cases/billing/__init__.py
Normal file
0
src/core/application/use_cases/billing/__init__.py
Normal file
0
src/core/application/use_cases/servers/__init__.py
Normal file
0
src/core/application/use_cases/servers/__init__.py
Normal file
0
src/core/application/use_cases/services/__init__.py
Normal file
0
src/core/application/use_cases/services/__init__.py
Normal file
0
src/core/application/validators/__init__.py
Normal file
0
src/core/application/validators/__init__.py
Normal file
0
src/core/domain/__init__.py
Normal file
0
src/core/domain/__init__.py
Normal file
0
src/core/domain/entities/__init__.py
Normal file
0
src/core/domain/entities/__init__.py
Normal file
67
src/core/domain/entities/base.py
Normal file
67
src/core/domain/entities/base.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Base entity for domain layer.
|
||||
|
||||
This module defines the base entity class that all domain entities inherit from.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class BaseEntity:
|
||||
"""Base domain entity.
|
||||
|
||||
All domain entities should inherit from this class.
|
||||
This is NOT a database model, but a pure domain object.
|
||||
|
||||
Attributes:
|
||||
id: Entity identifier
|
||||
created_at: Creation timestamp
|
||||
updated_at: Last update timestamp
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: Optional[int] = None,
|
||||
created_at: Optional[datetime] = None,
|
||||
updated_at: Optional[datetime] = None
|
||||
):
|
||||
"""Initialize base entity.
|
||||
|
||||
Args:
|
||||
id: Entity identifier
|
||||
created_at: Creation timestamp
|
||||
updated_at: Last update timestamp
|
||||
"""
|
||||
self.id = id
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
self.updated_at = updated_at or datetime.utcnow()
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Check equality based on ID.
|
||||
|
||||
Args:
|
||||
other: Other object
|
||||
|
||||
Returns:
|
||||
bool: True if same entity
|
||||
"""
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self.id == other.id
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Hash based on ID.
|
||||
|
||||
Returns:
|
||||
int: Hash value
|
||||
"""
|
||||
return hash(self.id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation.
|
||||
|
||||
Returns:
|
||||
str: String representation
|
||||
"""
|
||||
return f"<{self.__class__.__name__}(id={self.id})>"
|
||||
|
||||
0
src/core/domain/enums/__init__.py
Normal file
0
src/core/domain/enums/__init__.py
Normal file
0
src/core/domain/exceptions/__init__.py
Normal file
0
src/core/domain/exceptions/__init__.py
Normal file
59
src/core/domain/exceptions/base.py
Normal file
59
src/core/domain/exceptions/base.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Base exceptions for domain layer.
|
||||
|
||||
This module defines the exception hierarchy for the domain layer.
|
||||
"""
|
||||
|
||||
|
||||
class PeikarbandException(Exception):
|
||||
"""Base exception for all custom exceptions."""
|
||||
|
||||
def __init__(self, message: str, code: str = "INTERNAL_ERROR"):
|
||||
"""Initialize exception.
|
||||
|
||||
Args:
|
||||
message: Error message
|
||||
code: Error code
|
||||
"""
|
||||
self.message = message
|
||||
self.code = code
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class DomainException(PeikarbandException):
|
||||
"""Base exception for domain layer."""
|
||||
|
||||
def __init__(self, message: str, code: str = "DOMAIN_ERROR"):
|
||||
"""Initialize domain exception.
|
||||
|
||||
Args:
|
||||
message: Error message
|
||||
code: Error code
|
||||
"""
|
||||
super().__init__(message, code)
|
||||
|
||||
|
||||
class ValidationException(DomainException):
|
||||
"""Validation error in domain."""
|
||||
|
||||
def __init__(self, message: str, field: str = ""):
|
||||
"""Initialize validation exception.
|
||||
|
||||
Args:
|
||||
message: Error message
|
||||
field: Field that failed validation
|
||||
"""
|
||||
self.field = field
|
||||
super().__init__(message, "VALIDATION_ERROR")
|
||||
|
||||
|
||||
class BusinessRuleException(DomainException):
|
||||
"""Business rule violation."""
|
||||
|
||||
def __init__(self, message: str):
|
||||
"""Initialize business rule exception.
|
||||
|
||||
Args:
|
||||
message: Error message
|
||||
"""
|
||||
super().__init__(message, "BUSINESS_RULE_VIOLATION")
|
||||
|
||||
104
src/core/domain/exceptions/user_exceptions.py
Normal file
104
src/core/domain/exceptions/user_exceptions.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""User-related domain exceptions."""
|
||||
|
||||
from src.core.domain.exceptions.base import DomainException, ValidationException
|
||||
|
||||
|
||||
class UserNotFoundException(DomainException):
|
||||
"""User not found exception."""
|
||||
|
||||
def __init__(self, user_id: int = None, email: str = None):
|
||||
"""Initialize exception.
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
email: User email
|
||||
"""
|
||||
if user_id:
|
||||
message = f"User with ID {user_id} not found"
|
||||
elif email:
|
||||
message = f"User with email {email} not found"
|
||||
else:
|
||||
message = "User not found"
|
||||
super().__init__(message, "USER_NOT_FOUND")
|
||||
|
||||
|
||||
class EmailAlreadyExistsException(DomainException):
|
||||
"""Email already exists exception."""
|
||||
|
||||
def __init__(self, email: str):
|
||||
"""Initialize exception.
|
||||
|
||||
Args:
|
||||
email: Email address
|
||||
"""
|
||||
super().__init__(
|
||||
f"Email {email} is already registered",
|
||||
"EMAIL_ALREADY_EXISTS"
|
||||
)
|
||||
|
||||
|
||||
class InvalidEmailException(ValidationException):
|
||||
"""Invalid email format exception."""
|
||||
|
||||
def __init__(self, email: str):
|
||||
"""Initialize exception.
|
||||
|
||||
Args:
|
||||
email: Invalid email
|
||||
"""
|
||||
super().__init__(
|
||||
f"Invalid email format: {email}",
|
||||
field="email"
|
||||
)
|
||||
|
||||
|
||||
class WeakPasswordException(ValidationException):
|
||||
"""Weak password exception."""
|
||||
|
||||
def __init__(self, reason: str = "Password does not meet requirements"):
|
||||
"""Initialize exception.
|
||||
|
||||
Args:
|
||||
reason: Reason why password is weak
|
||||
"""
|
||||
super().__init__(reason, field="password")
|
||||
|
||||
|
||||
class InvalidCredentialsException(DomainException):
|
||||
"""Invalid login credentials exception."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize exception."""
|
||||
super().__init__(
|
||||
"Invalid email or password",
|
||||
"INVALID_CREDENTIALS"
|
||||
)
|
||||
|
||||
|
||||
class AccountLockedException(DomainException):
|
||||
"""Account is locked exception."""
|
||||
|
||||
def __init__(self, reason: str = "Account is locked"):
|
||||
"""Initialize exception.
|
||||
|
||||
Args:
|
||||
reason: Reason for lock
|
||||
"""
|
||||
super().__init__(reason, "ACCOUNT_LOCKED")
|
||||
|
||||
|
||||
class InsufficientBalanceException(DomainException):
|
||||
"""Insufficient wallet balance exception."""
|
||||
|
||||
def __init__(self, required: str, available: str):
|
||||
"""Initialize exception.
|
||||
|
||||
Args:
|
||||
required: Required amount
|
||||
available: Available amount
|
||||
"""
|
||||
super().__init__(
|
||||
f"Insufficient balance. Required: {required}, Available: {available}",
|
||||
"INSUFFICIENT_BALANCE"
|
||||
)
|
||||
|
||||
0
src/core/domain/value_objects/__init__.py
Normal file
0
src/core/domain/value_objects/__init__.py
Normal file
94
src/core/domain/value_objects/email.py
Normal file
94
src/core/domain/value_objects/email.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Email value object.
|
||||
|
||||
Email is an immutable value object that represents a validated email address.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Email:
|
||||
"""Email value object.
|
||||
|
||||
Represents a validated email address.
|
||||
Immutable - once created, cannot be changed.
|
||||
|
||||
Attributes:
|
||||
value: The email address string
|
||||
"""
|
||||
|
||||
EMAIL_REGEX = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
|
||||
|
||||
def __init__(self, value: str):
|
||||
"""Initialize email.
|
||||
|
||||
Args:
|
||||
value: Email address string
|
||||
|
||||
Raises:
|
||||
ValueError: If email format is invalid
|
||||
"""
|
||||
if not self._is_valid(value):
|
||||
raise ValueError(f"Invalid email format: {value}")
|
||||
self._value = value.lower().strip()
|
||||
|
||||
@property
|
||||
def value(self) -> str:
|
||||
"""Get email value.
|
||||
|
||||
Returns:
|
||||
str: Email address
|
||||
"""
|
||||
return self._value
|
||||
|
||||
@staticmethod
|
||||
def _is_valid(email: str) -> bool:
|
||||
"""Validate email format.
|
||||
|
||||
Args:
|
||||
email: Email string to validate
|
||||
|
||||
Returns:
|
||||
bool: True if valid
|
||||
"""
|
||||
if not email or not isinstance(email, str):
|
||||
return False
|
||||
return bool(Email.EMAIL_REGEX.match(email.strip()))
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation.
|
||||
|
||||
Returns:
|
||||
str: Email address
|
||||
"""
|
||||
return self._value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Developer representation.
|
||||
|
||||
Returns:
|
||||
str: Email representation
|
||||
"""
|
||||
return f"Email('{self._value}')"
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Check equality.
|
||||
|
||||
Args:
|
||||
other: Other object
|
||||
|
||||
Returns:
|
||||
bool: True if equal
|
||||
"""
|
||||
if not isinstance(other, Email):
|
||||
return False
|
||||
return self._value == other._value
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Hash value.
|
||||
|
||||
Returns:
|
||||
int: Hash
|
||||
"""
|
||||
return hash(self._value)
|
||||
|
||||
202
src/core/domain/value_objects/money.py
Normal file
202
src/core/domain/value_objects/money.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""Money value object.
|
||||
|
||||
Money represents a monetary amount with currency.
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any, Union
|
||||
|
||||
|
||||
class Money:
|
||||
"""Money value object.
|
||||
|
||||
Represents a monetary amount with currency.
|
||||
Immutable and provides monetary operations.
|
||||
|
||||
Attributes:
|
||||
amount: Decimal amount
|
||||
currency: Currency code (e.g., 'IRR', 'USD')
|
||||
"""
|
||||
|
||||
def __init__(self, amount: Union[Decimal, int, float, str], currency: str = "IRR"):
|
||||
"""Initialize money.
|
||||
|
||||
Args:
|
||||
amount: Monetary amount
|
||||
currency: Currency code (default: IRR for Iranian Rial)
|
||||
|
||||
Raises:
|
||||
ValueError: If amount is negative or currency is invalid
|
||||
"""
|
||||
self._amount = Decimal(str(amount))
|
||||
|
||||
if self._amount < 0:
|
||||
raise ValueError("Amount cannot be negative")
|
||||
|
||||
if not currency or len(currency) != 3:
|
||||
raise ValueError(f"Invalid currency code: {currency}")
|
||||
|
||||
self._currency = currency.upper()
|
||||
|
||||
@property
|
||||
def amount(self) -> Decimal:
|
||||
"""Get amount.
|
||||
|
||||
Returns:
|
||||
Decimal: Amount
|
||||
"""
|
||||
return self._amount
|
||||
|
||||
@property
|
||||
def currency(self) -> str:
|
||||
"""Get currency.
|
||||
|
||||
Returns:
|
||||
str: Currency code
|
||||
"""
|
||||
return self._currency
|
||||
|
||||
def add(self, other: "Money") -> "Money":
|
||||
"""Add two money values.
|
||||
|
||||
Args:
|
||||
other: Other money value
|
||||
|
||||
Returns:
|
||||
Money: New money object with sum
|
||||
|
||||
Raises:
|
||||
ValueError: If currencies don't match
|
||||
"""
|
||||
self._check_currency(other)
|
||||
return Money(self._amount + other._amount, self._currency)
|
||||
|
||||
def subtract(self, other: "Money") -> "Money":
|
||||
"""Subtract money value.
|
||||
|
||||
Args:
|
||||
other: Other money value
|
||||
|
||||
Returns:
|
||||
Money: New money object with difference
|
||||
|
||||
Raises:
|
||||
ValueError: If currencies don't match or result is negative
|
||||
"""
|
||||
self._check_currency(other)
|
||||
result = self._amount - other._amount
|
||||
if result < 0:
|
||||
raise ValueError("Result cannot be negative")
|
||||
return Money(result, self._currency)
|
||||
|
||||
def multiply(self, multiplier: Union[int, float, Decimal]) -> "Money":
|
||||
"""Multiply amount by a factor.
|
||||
|
||||
Args:
|
||||
multiplier: Multiplication factor
|
||||
|
||||
Returns:
|
||||
Money: New money object
|
||||
"""
|
||||
result = self._amount * Decimal(str(multiplier))
|
||||
return Money(result, self._currency)
|
||||
|
||||
def _check_currency(self, other: "Money") -> None:
|
||||
"""Check if currencies match.
|
||||
|
||||
Args:
|
||||
other: Other money value
|
||||
|
||||
Raises:
|
||||
ValueError: If currencies don't match
|
||||
"""
|
||||
if self._currency != other._currency:
|
||||
raise ValueError(
|
||||
f"Currency mismatch: {self._currency} vs {other._currency}"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation.
|
||||
|
||||
Returns:
|
||||
str: Formatted money string
|
||||
"""
|
||||
return f"{self._amount:,.2f} {self._currency}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Developer representation.
|
||||
|
||||
Returns:
|
||||
str: Money representation
|
||||
"""
|
||||
return f"Money({self._amount}, '{self._currency}')"
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Check equality.
|
||||
|
||||
Args:
|
||||
other: Other object
|
||||
|
||||
Returns:
|
||||
bool: True if equal
|
||||
"""
|
||||
if not isinstance(other, Money):
|
||||
return False
|
||||
return self._amount == other._amount and self._currency == other._currency
|
||||
|
||||
def __lt__(self, other: "Money") -> bool:
|
||||
"""Less than comparison.
|
||||
|
||||
Args:
|
||||
other: Other money value
|
||||
|
||||
Returns:
|
||||
bool: True if less than
|
||||
"""
|
||||
self._check_currency(other)
|
||||
return self._amount < other._amount
|
||||
|
||||
def __le__(self, other: "Money") -> bool:
|
||||
"""Less than or equal comparison.
|
||||
|
||||
Args:
|
||||
other: Other money value
|
||||
|
||||
Returns:
|
||||
bool: True if less than or equal
|
||||
"""
|
||||
self._check_currency(other)
|
||||
return self._amount <= other._amount
|
||||
|
||||
def __gt__(self, other: "Money") -> bool:
|
||||
"""Greater than comparison.
|
||||
|
||||
Args:
|
||||
other: Other money value
|
||||
|
||||
Returns:
|
||||
bool: True if greater than
|
||||
"""
|
||||
self._check_currency(other)
|
||||
return self._amount > other._amount
|
||||
|
||||
def __ge__(self, other: "Money") -> bool:
|
||||
"""Greater than or equal comparison.
|
||||
|
||||
Args:
|
||||
other: Other money value
|
||||
|
||||
Returns:
|
||||
bool: True if greater than or equal
|
||||
"""
|
||||
self._check_currency(other)
|
||||
return self._amount >= other._amount
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Hash value.
|
||||
|
||||
Returns:
|
||||
int: Hash
|
||||
"""
|
||||
return hash((self._amount, self._currency))
|
||||
|
||||
140
src/core/domain/value_objects/phone.py
Normal file
140
src/core/domain/value_objects/phone.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""Phone number value object.
|
||||
|
||||
Phone represents a validated phone number.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Phone:
|
||||
"""Phone number value object.
|
||||
|
||||
Represents a validated Iranian phone number.
|
||||
Supports both mobile and landline numbers.
|
||||
|
||||
Attributes:
|
||||
value: Phone number string (normalized format)
|
||||
"""
|
||||
|
||||
# Iranian mobile: starts with 09, 10 digits
|
||||
MOBILE_REGEX = re.compile(r'^09\d{9}$')
|
||||
|
||||
# International format: +989...
|
||||
INTERNATIONAL_REGEX = re.compile(r'^\+989\d{9}$')
|
||||
|
||||
def __init__(self, value: str):
|
||||
"""Initialize phone number.
|
||||
|
||||
Args:
|
||||
value: Phone number string
|
||||
|
||||
Raises:
|
||||
ValueError: If phone format is invalid
|
||||
"""
|
||||
normalized = self._normalize(value)
|
||||
if not self._is_valid(normalized):
|
||||
raise ValueError(f"Invalid phone number: {value}")
|
||||
self._value = normalized
|
||||
|
||||
@property
|
||||
def value(self) -> str:
|
||||
"""Get phone number.
|
||||
|
||||
Returns:
|
||||
str: Phone number
|
||||
"""
|
||||
return self._value
|
||||
|
||||
@property
|
||||
def international_format(self) -> str:
|
||||
"""Get international format.
|
||||
|
||||
Returns:
|
||||
str: +989XXXXXXXXX format
|
||||
"""
|
||||
if self._value.startswith('+'):
|
||||
return self._value
|
||||
return f"+98{self._value[1:]}"
|
||||
|
||||
@staticmethod
|
||||
def _normalize(phone: str) -> str:
|
||||
"""Normalize phone number.
|
||||
|
||||
Removes spaces, dashes, parentheses.
|
||||
|
||||
Args:
|
||||
phone: Phone string
|
||||
|
||||
Returns:
|
||||
str: Normalized phone
|
||||
"""
|
||||
if not phone:
|
||||
return ""
|
||||
|
||||
# Remove common separators
|
||||
phone = re.sub(r'[\s\-\(\)]', '', phone.strip())
|
||||
|
||||
# Convert international format to local
|
||||
if phone.startswith('+98'):
|
||||
phone = '0' + phone[3:]
|
||||
elif phone.startswith('0098'):
|
||||
phone = '0' + phone[4:]
|
||||
elif phone.startswith('98') and len(phone) == 12:
|
||||
phone = '0' + phone[2:]
|
||||
|
||||
return phone
|
||||
|
||||
@staticmethod
|
||||
def _is_valid(phone: str) -> bool:
|
||||
"""Validate phone number.
|
||||
|
||||
Args:
|
||||
phone: Normalized phone string
|
||||
|
||||
Returns:
|
||||
bool: True if valid
|
||||
"""
|
||||
if not phone or not isinstance(phone, str):
|
||||
return False
|
||||
|
||||
# Check if matches mobile pattern
|
||||
return bool(Phone.MOBILE_REGEX.match(phone))
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation.
|
||||
|
||||
Returns:
|
||||
str: Phone number
|
||||
"""
|
||||
return self._value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Developer representation.
|
||||
|
||||
Returns:
|
||||
str: Phone representation
|
||||
"""
|
||||
return f"Phone('{self._value}')"
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Check equality.
|
||||
|
||||
Args:
|
||||
other: Other object
|
||||
|
||||
Returns:
|
||||
bool: True if equal
|
||||
"""
|
||||
if not isinstance(other, Phone):
|
||||
return False
|
||||
return self._value == other._value
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Hash value.
|
||||
|
||||
Returns:
|
||||
int: Hash
|
||||
"""
|
||||
return hash(self._value)
|
||||
|
||||
0
src/core/utils/__init__.py
Normal file
0
src/core/utils/__init__.py
Normal file
0
src/infrastructure/__init__.py
Normal file
0
src/infrastructure/__init__.py
Normal file
0
src/infrastructure/cache/__init__.py
vendored
Normal file
0
src/infrastructure/cache/__init__.py
vendored
Normal file
0
src/infrastructure/database/__init__.py
Normal file
0
src/infrastructure/database/__init__.py
Normal file
0
src/infrastructure/database/migrations/__init__.py
Normal file
0
src/infrastructure/database/migrations/__init__.py
Normal file
87
src/infrastructure/database/migrations/env.py
Normal file
87
src/infrastructure/database/migrations/env.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Alembic environment configuration.
|
||||
|
||||
This module is used by Alembic to manage database migrations.
|
||||
"""
|
||||
|
||||
from logging.config import fileConfig
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from alembic import context
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
project_root = Path(__file__).parents[4]
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.config.settings import settings
|
||||
from src.config.database import Base
|
||||
|
||||
# Import all models to ensure they're registered with Base.metadata
|
||||
from src.infrastructure.database.models import * # noqa
|
||||
|
||||
# Alembic Config object
|
||||
config = context.config
|
||||
|
||||
# Set database URL from settings
|
||||
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
|
||||
|
||||
# Interpret the config file for Python logging
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# Target metadata for autogenerate
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
compare_type=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
compare_type=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
|
||||
27
src/infrastructure/database/migrations/script.py.mako
Normal file
27
src/infrastructure/database/migrations/script.py.mako
Normal file
@@ -0,0 +1,27 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade database schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade database schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
|
||||
0
src/infrastructure/database/models/__init__.py
Normal file
0
src/infrastructure/database/models/__init__.py
Normal file
51
src/infrastructure/database/models/base.py
Normal file
51
src/infrastructure/database/models/base.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Base model for all database models.
|
||||
|
||||
This module defines the base class that all SQLAlchemy models inherit from.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, DateTime
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
|
||||
from src.config.database import Base as DeclarativeBase
|
||||
|
||||
|
||||
class BaseModel(DeclarativeBase):
|
||||
"""Base model with common fields.
|
||||
|
||||
All models should inherit from this class to get:
|
||||
- id (primary key)
|
||||
- created_at
|
||||
- updated_at
|
||||
- Automatic table naming
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(
|
||||
DateTime,
|
||||
default=datetime.utcnow,
|
||||
onupdate=datetime.utcnow,
|
||||
nullable=False
|
||||
)
|
||||
|
||||
@declared_attr
|
||||
def __tablename__(cls) -> str:
|
||||
"""Generate table name from class name.
|
||||
|
||||
Converts PascalCase to snake_case.
|
||||
Example: UserModel -> user_model
|
||||
"""
|
||||
import re
|
||||
name = cls.__name__
|
||||
# Convert PascalCase to snake_case
|
||||
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
|
||||
return name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation."""
|
||||
return f"<{self.__class__.__name__}(id={self.id})>"
|
||||
|
||||
122
src/infrastructure/database/repositories/base_repository.py
Normal file
122
src/infrastructure/database/repositories/base_repository.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""Base repository implementation.
|
||||
|
||||
This module provides a base repository class with common CRUD operations.
|
||||
"""
|
||||
|
||||
from typing import TypeVar, Generic, Optional, List, Type
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.infrastructure.database.models.base import BaseModel
|
||||
|
||||
|
||||
ModelType = TypeVar("ModelType", bound=BaseModel)
|
||||
|
||||
|
||||
class BaseRepository(Generic[ModelType]):
|
||||
"""Base repository with common database operations.
|
||||
|
||||
Provides basic CRUD operations that can be inherited by specific repositories.
|
||||
|
||||
Attributes:
|
||||
model: SQLAlchemy model class
|
||||
session: Database session
|
||||
"""
|
||||
|
||||
def __init__(self, model: Type[ModelType], session: Session):
|
||||
"""Initialize repository.
|
||||
|
||||
Args:
|
||||
model: SQLAlchemy model class
|
||||
session: Database session
|
||||
"""
|
||||
self.model = model
|
||||
self._session = session
|
||||
|
||||
def get_by_id(self, id: int) -> Optional[ModelType]:
|
||||
"""Get entity by ID.
|
||||
|
||||
Args:
|
||||
id: Entity ID
|
||||
|
||||
Returns:
|
||||
Optional[ModelType]: Entity or None
|
||||
"""
|
||||
return self._session.query(self.model).filter(self.model.id == id).first()
|
||||
|
||||
def get_all(self, skip: int = 0, limit: int = 100) -> List[ModelType]:
|
||||
"""Get all entities with pagination.
|
||||
|
||||
Args:
|
||||
skip: Number of records to skip
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
List[ModelType]: List of entities
|
||||
"""
|
||||
return self._session.query(self.model).offset(skip).limit(limit).all()
|
||||
|
||||
def create(self, entity: ModelType) -> ModelType:
|
||||
"""Create new entity.
|
||||
|
||||
Args:
|
||||
entity: Entity to create
|
||||
|
||||
Returns:
|
||||
ModelType: Created entity
|
||||
"""
|
||||
self._session.add(entity)
|
||||
self._session.flush()
|
||||
self._session.refresh(entity)
|
||||
return entity
|
||||
|
||||
def update(self, entity: ModelType) -> ModelType:
|
||||
"""Update existing entity.
|
||||
|
||||
Args:
|
||||
entity: Entity to update
|
||||
|
||||
Returns:
|
||||
ModelType: Updated entity
|
||||
"""
|
||||
self._session.merge(entity)
|
||||
self._session.flush()
|
||||
self._session.refresh(entity)
|
||||
return entity
|
||||
|
||||
def delete(self, id: int) -> bool:
|
||||
"""Delete entity by ID.
|
||||
|
||||
Args:
|
||||
id: Entity ID
|
||||
|
||||
Returns:
|
||||
bool: True if deleted, False if not found
|
||||
"""
|
||||
entity = self.get_by_id(id)
|
||||
if entity:
|
||||
self._session.delete(entity)
|
||||
self._session.flush()
|
||||
return True
|
||||
return False
|
||||
|
||||
def count(self) -> int:
|
||||
"""Count total number of entities.
|
||||
|
||||
Returns:
|
||||
int: Total count
|
||||
"""
|
||||
return self._session.query(self.model).count()
|
||||
|
||||
def exists(self, id: int) -> bool:
|
||||
"""Check if entity exists.
|
||||
|
||||
Args:
|
||||
id: Entity ID
|
||||
|
||||
Returns:
|
||||
bool: True if exists
|
||||
"""
|
||||
return self._session.query(
|
||||
self._session.query(self.model).filter(self.model.id == id).exists()
|
||||
).scalar()
|
||||
|
||||
81
src/infrastructure/database/unit_of_work.py
Normal file
81
src/infrastructure/database/unit_of_work.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Unit of Work pattern implementation.
|
||||
|
||||
This module implements the Unit of Work pattern for managing database transactions.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.config.database import SessionLocal
|
||||
|
||||
|
||||
class UnitOfWork:
|
||||
"""Unit of Work for managing database transactions.
|
||||
|
||||
Usage:
|
||||
with UnitOfWork() as uow:
|
||||
user = uow.users.get_by_id(1)
|
||||
user.email = "new@email.com"
|
||||
uow.users.update(user)
|
||||
# Automatically commits on success
|
||||
"""
|
||||
|
||||
def __init__(self, session_factory=SessionLocal):
|
||||
"""Initialize Unit of Work.
|
||||
|
||||
Args:
|
||||
session_factory: Factory to create database sessions
|
||||
"""
|
||||
self.session_factory = session_factory
|
||||
self._session: Optional[Session] = None
|
||||
|
||||
def __enter__(self) -> "UnitOfWork":
|
||||
"""Enter context manager.
|
||||
|
||||
Returns:
|
||||
UnitOfWork: Self
|
||||
"""
|
||||
self._session = self.session_factory()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Exit context manager.
|
||||
|
||||
Commits on success, rollbacks on error.
|
||||
"""
|
||||
if exc_type is not None:
|
||||
self.rollback()
|
||||
else:
|
||||
self.commit()
|
||||
self.close()
|
||||
|
||||
def commit(self) -> None:
|
||||
"""Commit the current transaction."""
|
||||
if self._session:
|
||||
self._session.commit()
|
||||
|
||||
def rollback(self) -> None:
|
||||
"""Rollback the current transaction."""
|
||||
if self._session:
|
||||
self._session.rollback()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the session."""
|
||||
if self._session:
|
||||
self._session.close()
|
||||
self._session = None
|
||||
|
||||
@property
|
||||
def session(self) -> Session:
|
||||
"""Get the current session.
|
||||
|
||||
Returns:
|
||||
Session: Current database session
|
||||
|
||||
Raises:
|
||||
RuntimeError: If session not initialized
|
||||
"""
|
||||
if self._session is None:
|
||||
raise RuntimeError("Session not initialized. Use within context manager.")
|
||||
return self._session
|
||||
|
||||
0
src/infrastructure/external/__init__.py
vendored
Normal file
0
src/infrastructure/external/__init__.py
vendored
Normal file
0
src/infrastructure/external/email/__init__.py
vendored
Normal file
0
src/infrastructure/external/email/__init__.py
vendored
Normal file
0
src/infrastructure/external/payment/__init__.py
vendored
Normal file
0
src/infrastructure/external/payment/__init__.py
vendored
Normal file
0
src/infrastructure/external/providers/__init__.py
vendored
Normal file
0
src/infrastructure/external/providers/__init__.py
vendored
Normal file
0
src/infrastructure/external/sms/__init__.py
vendored
Normal file
0
src/infrastructure/external/sms/__init__.py
vendored
Normal file
0
src/infrastructure/logging/__init__.py
Normal file
0
src/infrastructure/logging/__init__.py
Normal file
0
src/infrastructure/security/__init__.py
Normal file
0
src/infrastructure/security/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user