[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