first commit
This commit is contained in:
43
.env
Normal file
43
.env
Normal file
@@ -0,0 +1,43 @@
|
||||
# Application
|
||||
APP_NAME=VConnect API
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=True
|
||||
API_V1_PREFIX=/api
|
||||
|
||||
# Database
|
||||
DATABASE_URL=sqlite:///./vconnect.db
|
||||
# SQLite (개발용): sqlite:///./vconnect.db
|
||||
|
||||
# JWT Authentication
|
||||
JWT_SECRET_KEY=your-super-secret-key-change-this-in-production
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS=7
|
||||
|
||||
# Proxmox
|
||||
PROXMOX_HOST=https://192.168.0.115:8006
|
||||
PROXMOX_API_TOKEN=PVEAPIToken=root@pam!vconnect=d35fb41c-34fa-4fae-9d97-8d18af7b5525
|
||||
PROXMOX_VERIFY_SSL=False
|
||||
|
||||
# SSH Gateway (터널링 서버)
|
||||
SSH_HOST=192.168.0.97
|
||||
SSH_PORT=22
|
||||
SSH_USERNAME=kdesk84
|
||||
SSH_KEY_PATH=/data/vconnect-api/.ssh/id_rsa
|
||||
# 또는 SSH_PASSWORD=your-password
|
||||
|
||||
# Port Range for Tunneling
|
||||
TUNNEL_PORT_MIN=50000
|
||||
TUNNEL_PORT_MAX=60000
|
||||
|
||||
# CORS (클라이언트 도메인)
|
||||
CORS_ORIGINS=["http://localhost:8080", "http://localhost:3000"]
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FILE=logs/vconnect.log
|
||||
|
||||
# Admin (첫 관리자 계정)
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin1234
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
43
.env.example
Normal file
43
.env.example
Normal file
@@ -0,0 +1,43 @@
|
||||
# Application
|
||||
APP_NAME=VConnect API
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=True
|
||||
API_V1_PREFIX=/api
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://vconnect:vconnect123@localhost:5432/vconnect
|
||||
# SQLite (개발용): sqlite:///./vconnect.db
|
||||
|
||||
# JWT Authentication
|
||||
JWT_SECRET_KEY=your-super-secret-key-change-this-in-production
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS=7
|
||||
|
||||
# Proxmox
|
||||
PROXMOX_HOST=https://pve.mouse84.com:8006
|
||||
PROXMOX_API_TOKEN=PVEAPIToken=root@pam!vconnect=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
PROXMOX_VERIFY_SSL=False
|
||||
|
||||
# SSH Gateway (터널링 서버)
|
||||
SSH_HOST=ssh.example.com
|
||||
SSH_PORT=22
|
||||
SSH_USERNAME=tunneluser
|
||||
SSH_KEY_PATH=/path/to/ssh/private_key
|
||||
# 또는 SSH_PASSWORD=your-password
|
||||
|
||||
# Port Range for Tunneling
|
||||
TUNNEL_PORT_MIN=50000
|
||||
TUNNEL_PORT_MAX=60000
|
||||
|
||||
# CORS (클라이언트 도메인)
|
||||
CORS_ORIGINS=["http://localhost:8080", "http://localhost:3000"]
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FILE=logs/vconnect.log
|
||||
|
||||
# Admin (첫 관리자 계정)
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin123
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
49
.ssh/id_rsa
Normal file
49
.ssh/id_rsa
Normal file
@@ -0,0 +1,49 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAgEAxujXqcl/QrtdHd1fGSJQmRR1/McRkLJSE9nYwne27XAjW27xXArc
|
||||
giqSSgU+kh4MdjFJJ57iFg2L3ekZ86xdjvsuzzk1rSVIq6f3TA3Wg+apSrx4ny1Z53K3+u
|
||||
YAwiMHcsIgv3C1zW8s+gY48Rtf7fnMYwZqrE4pr+M6beccv9LfnCi01oPomTCQagtXlTkU
|
||||
rIm9fsu1VNHn3TkUAmKS8WRAMthzYLthmekl2OkfTpepSIT0xWuEv9Ny6eY+KzZeiF5NM3
|
||||
slTnImD6RpJSjt34sdn3Ng06C0LD9nHDkeUBAZ0TMoiu5arA0jjsf9hOFumCUX3hyaHtm8
|
||||
O19G3ZVZHnEa87Wjx+rHOyeTAjmC5IGGivDhyiy1ehWQw/wR9go54m95IT9rDKObpl5bQW
|
||||
BeSn2Nid586kMd0qILEOz5Ziqpnl5XulbumFvZkG/SR1ef7cyaykYyfJp0XmjFd8YKlBS9
|
||||
fF10DcOQjhN389X4pblY1CavpvCE2oN+8ijDHUGR40PJBrCMDCnradGf3lB/5m6eKTJfhO
|
||||
UtEz5iQYf8CoCVvpUU85j8648f2f7gxz9L9yhckwpMlUvuxI1yuSAehaD9u1pklECS1Sbt
|
||||
vcp7a6O6WCHn/wolOPzOcleB9dXVziQUpCf8L/p+K7cFvM5tqawLuzOKFbk6FXNTMMVRcb
|
||||
8AAAdIdfeBDHX3gQwAAAAHc3NoLXJzYQAAAgEAxujXqcl/QrtdHd1fGSJQmRR1/McRkLJS
|
||||
E9nYwne27XAjW27xXArcgiqSSgU+kh4MdjFJJ57iFg2L3ekZ86xdjvsuzzk1rSVIq6f3TA
|
||||
3Wg+apSrx4ny1Z53K3+uYAwiMHcsIgv3C1zW8s+gY48Rtf7fnMYwZqrE4pr+M6beccv9Lf
|
||||
nCi01oPomTCQagtXlTkUrIm9fsu1VNHn3TkUAmKS8WRAMthzYLthmekl2OkfTpepSIT0xW
|
||||
uEv9Ny6eY+KzZeiF5NM3slTnImD6RpJSjt34sdn3Ng06C0LD9nHDkeUBAZ0TMoiu5arA0j
|
||||
jsf9hOFumCUX3hyaHtm8O19G3ZVZHnEa87Wjx+rHOyeTAjmC5IGGivDhyiy1ehWQw/wR9g
|
||||
o54m95IT9rDKObpl5bQWBeSn2Nid586kMd0qILEOz5Ziqpnl5XulbumFvZkG/SR1ef7cya
|
||||
ykYyfJp0XmjFd8YKlBS9fF10DcOQjhN389X4pblY1CavpvCE2oN+8ijDHUGR40PJBrCMDC
|
||||
nradGf3lB/5m6eKTJfhOUtEz5iQYf8CoCVvpUU85j8648f2f7gxz9L9yhckwpMlUvuxI1y
|
||||
uSAehaD9u1pklECS1Sbtvcp7a6O6WCHn/wolOPzOcleB9dXVziQUpCf8L/p+K7cFvM5tqa
|
||||
wLuzOKFbk6FXNTMMVRcb8AAAADAQABAAACABoGuG/2YLObbs0sEeKThgaUUfXfxEbHFmHo
|
||||
5L8D9AhbCRKl0Gici+AaRZGa4GY1DqizvTXfyQcIAbo+Yw9qXm7eze/+X9gbzfJBTuSBgQ
|
||||
U5kVgpPgKHPWwWgMflqslm1PV+kii1IjdS/zMTIls7RjuOA7Yco6jlAfkKMIiAhNdSoRXp
|
||||
O57qm+zYw7n5vvLUXltPGzTeibJN/tqIOpEvUw5zxsoAXrm++uqCbQSAnas7wUIvEhSiLg
|
||||
39fbE+LPnxf2q00BbW26RJNP1h+VdhOh1rB1a9kuNN8+qVkd50WNOIBSDFjV3/CcKDu5iA
|
||||
nDBJYPOrvX+UsCkuOz4XiDRmc3EC8QyKrTEwUODwQCJNU3QMyEisCzUUpgYxj/X83ZSe5w
|
||||
zpazGXm/YBvijTCWxR/tmy/t1226ORzR7wNVQTw9JcPQ+HjKYlaWu2hkVlVnnRgyKwje8v
|
||||
p2VXI2Qq2QOIjeJtneI8rUMZgv0H4N503PNnPuu+xTdy0Bzvuscesuf9NDcZfIwD95Jtdt
|
||||
//n1so54ebVWqUxRHvDxFuCLp5OlCD3lJG1HHz4WT6MUiA2SL9znJt65myOhPyupILwSnH
|
||||
33+9fyZhn9eYUapzJOSizJvbwQbnx1AnkvTKnz+sFfnJTiD/er5ChZdwlzUQd36oBn8qBh
|
||||
DmUp59t/uzcDTMrtGdAAABAG7cy3jp5qnEU249IktkD3uPU0N/6JETMSUyRu2DdwI3RTol
|
||||
KdscxICvK3E08ysOxjX/8kU6UqGirWjCIR7IHvjbLNOrKj/EXkeDyVA0HqmJ+tr0FIe4gR
|
||||
qGQ016uS6uUANh2Bb8zicDu9e2VDuWkkq3h2XR+UGC8lUQn/OeoiDVb7hVxCbdFQwzmQHo
|
||||
74+VVYCdYp2B9qJ6MyMdc92rc2dfamYj/eVitJz6Rhs3COOPJOOrAvrykkst4BtS4YZuzM
|
||||
vuSYmjSii0TA9HIZXcdxZ+VjEbutrNkd467x44KBC6bzo+xVgn9vnyEDxQEBCKvNdDb5y9
|
||||
94QNndRGzYumSXoAAAEBAO8gcxXRUdlHtSWHml7+6XJiCmUSIgzCEmOxoG7C+hNWbc6pDc
|
||||
WYvj1w3QNJYb+DQg+aHsUI5dAMSuLgZvIHogDoHSKy5mkoIb9uIS1vqt9UiY9XajuPPb7u
|
||||
uJaxI6K8h7Xyc6z9f3gtajov4QDM6w5pyvZJjrzwmPqO6IQhcd7EWlpl+FOGVc0OFupn3M
|
||||
0dl9GVxtfZq0GAD3W5mum1PkUjgBYt22hzXmQU1uqCqcoivPS7v3fHPWj/Uh+4ZzM5O7xk
|
||||
WyGkt6RVosUfB1lNBuYxncy+SVVGlDLzwg5VVhwfX16jdA8ntVL8C7RmbO3jcGiEbVsSKp
|
||||
cue33/5j75Ui0AAAEBANTx6j/uIS3kVOZc8whQOSC3Z0Fa02RbeDnmb8S/68Y3iz5EijCW
|
||||
w8Cu85zZlCz5O+lnWel4Zy6oe6k/AXVm8CaRoafeMhw7AhvZth0bV0ZEIY98g0k/jzUkCX
|
||||
6sQKBjnjw0DGtRLkboFkY4cfRUsgJe0C2E4c1iHoqLGNbcCGB0CNLUAJm1Cn6FPeQrB/Lo
|
||||
F0TRy1u2ZWPwMNhITtR3nb1ZbnDt96Jp5BMRVxLn1bDx3yRd/Zi1EvXNYr7p3aoLhJDCKN
|
||||
rZHIRl6gR8FFDulirMANZ+CxntDBCZ6Y9dozHrvgT+TIZRVUwIRxkwnbHsgtt93NfH/xrd
|
||||
xLxm9mkUQxsAAAAPcm9vdEBhcHQtc2VydmVyAQIDBA==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
.ssh/id_rsa.pub
Normal file
1
.ssh/id_rsa.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDG6NepyX9Cu10d3V8ZIlCZFHX8xxGQslIT2djCd7btcCNbbvFcCtyCKpJKBT6SHgx2MUknnuIWDYvd6RnzrF2O+y7POTWtJUirp/dMDdaD5qlKvHifLVnncrf65gDCIwdywiC/cLXNbyz6BjjxG1/t+cxjBmqsTimv4zpt5xy/0t+cKLTWg+iZMJBqC1eVORSsib1+y7VU0efdORQCYpLxZEAy2HNgu2GZ6SXY6R9Ol6lIhPTFa4S/03Lp5j4rNl6IXk0zeyVOciYPpGklKO3fix2fc2DToLQsP2ccOR5QEBnRMyiK7lqsDSOOx/2E4W6YJRfeHJoe2bw7X0bdlVkecRrztaPH6sc7J5MCOYLkgYaK8OHKLLV6FZDD/BH2Cjnib3khP2sMo5umXltBYF5KfY2J3nzqQx3SogsQ7PlmKqmeXle6Vu6YW9mQb9JHV5/tzJrKRjJ8mnReaMV3xgqUFL18XXQNw5COE3fz1filuVjUJq+m8ITag37yKMMdQZHjQ8kGsIwMKetp0Z/eUH/mbp4pMl+E5S0TPmJBh/wKgJW+lRTzmPzrjx/Z/uDHP0v3KFyTCkyVS+7EjXK5IB6FoP27WmSUQJLVJu29yntro7pYIef/CiU4/M5yV4H11dXOJBSkJ/wv+n4rtwW8zm2prAu7M4oVuToVc1MwxVFxvw== root@apt-server
|
||||
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 의존성 복사 및 설치
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 애플리케이션 코드 복사
|
||||
COPY app ./app
|
||||
|
||||
# 포트 노출
|
||||
EXPOSE 8000
|
||||
|
||||
# 서버 실행
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
151
QUICKSTART.md
Normal file
151
QUICKSTART.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# VConnect API 서버 빠른 시작 가이드
|
||||
|
||||
## 1️⃣ 설치
|
||||
|
||||
```bash
|
||||
# 프로젝트 디렉토리로 이동
|
||||
cd vconnect-api
|
||||
|
||||
# 가상환경 생성 (선택사항이지만 권장)
|
||||
python -m venv venv
|
||||
|
||||
# 가상환경 활성화
|
||||
# Linux/Mac:
|
||||
source venv/bin/activate
|
||||
# Windows:
|
||||
venv\Scripts\activate
|
||||
|
||||
# 의존성 설치
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 2️⃣ 환경 설정
|
||||
|
||||
```bash
|
||||
# .env 파일 생성
|
||||
cp .env.example .env
|
||||
|
||||
# .env 파일 편집
|
||||
nano .env # 또는 원하는 에디터 사용
|
||||
```
|
||||
|
||||
### 필수 설정 항목:
|
||||
|
||||
```env
|
||||
# Proxmox 설정
|
||||
PROXMOX_HOST=https://pve.mouse84.com:8006
|
||||
PROXMOX_API_TOKEN=PVEAPIToken=root@pam!vconnect=YOUR-TOKEN-HERE
|
||||
|
||||
# SSH Gateway 설정 (터널링 서버)
|
||||
SSH_HOST=your-ssh-server.com
|
||||
SSH_PORT=22
|
||||
SSH_USERNAME=tunneluser
|
||||
SSH_KEY_PATH=/path/to/ssh/key
|
||||
# 또는
|
||||
SSH_PASSWORD=your-password
|
||||
|
||||
# JWT Secret (반드시 변경!)
|
||||
JWT_SECRET_KEY=your-super-secret-key-change-this
|
||||
```
|
||||
|
||||
## 3️⃣ 서버 실행
|
||||
|
||||
```bash
|
||||
# 개발 모드 (자동 리로드)
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
|
||||
# 프로덕션 모드
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||
```
|
||||
|
||||
## 4️⃣ 접속 확인
|
||||
|
||||
브라우저에서:
|
||||
- API Docs: http://localhost:8000/docs
|
||||
- Health Check: http://localhost:8000/health
|
||||
|
||||
## 5️⃣ 테스트
|
||||
|
||||
### 로그인 테스트
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin123"}'
|
||||
```
|
||||
|
||||
### VM 목록 조회
|
||||
```bash
|
||||
# 먼저 로그인해서 토큰 받기
|
||||
TOKEN="your-jwt-token-here"
|
||||
|
||||
curl -X GET http://localhost:8000/api/vms/my \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 6️⃣ Docker로 실행 (선택사항)
|
||||
|
||||
```bash
|
||||
# Docker 이미지 빌드
|
||||
docker build -t vconnect-api .
|
||||
|
||||
# 컨테이너 실행
|
||||
docker run -d \
|
||||
--name vconnect-api \
|
||||
-p 8000:8000 \
|
||||
--env-file .env \
|
||||
vconnect-api
|
||||
```
|
||||
|
||||
## 📋 기본 관리자 계정
|
||||
|
||||
- Username: `admin`
|
||||
- Password: `admin123`
|
||||
|
||||
⚠️ **보안을 위해 첫 로그인 후 반드시 비밀번호를 변경하세요!**
|
||||
|
||||
## 🔧 문제 해결
|
||||
|
||||
### Proxmox 연결 오류
|
||||
- API Token이 올바른지 확인
|
||||
- Proxmox 서버 URL과 포트 확인 (기본 8006)
|
||||
- 방화벽 설정 확인
|
||||
|
||||
### SSH 터널 오류
|
||||
- SSH 서버 접속 정보 확인
|
||||
- SSH 키 파일 경로 확인
|
||||
- SSH 서버에서 Port Forwarding 허용 여부 확인
|
||||
|
||||
### 포트 충돌
|
||||
- 8000 포트가 이미 사용 중이면 다른 포트로 변경:
|
||||
```bash
|
||||
uvicorn app.main:app --reload --port 8001
|
||||
```
|
||||
|
||||
## 📚 API 문서
|
||||
|
||||
서버 실행 후 http://localhost:8000/docs 에서 자동 생성된 API 문서를 확인할 수 있습니다.
|
||||
|
||||
### 주요 엔드포인트:
|
||||
|
||||
**인증**
|
||||
- POST /api/auth/register - 회원가입
|
||||
- POST /api/auth/login - 로그인
|
||||
- GET /api/auth/me - 현재 사용자 정보
|
||||
|
||||
**VM 관리**
|
||||
- GET /api/vms/my - 내 VM 목록
|
||||
- GET /api/vms/{vm_id} - VM 상세 정보
|
||||
- POST /api/vms/{vm_id}/start - VM 시작
|
||||
- POST /api/vms/{vm_id}/stop - VM 종료
|
||||
|
||||
**터널 관리**
|
||||
- POST /api/tunnel/create - SSH 터널 생성
|
||||
- GET /api/tunnel/{session_id}/status - 터널 상태
|
||||
- DELETE /api/tunnel/{session_id} - 터널 종료
|
||||
|
||||
## 🎯 다음 단계
|
||||
|
||||
1. WPF 클라이언트와 연동
|
||||
2. 사용자별 VM 접근 권한 설정
|
||||
3. 감사 로그 확인
|
||||
4. 프로덕션 배포
|
||||
156
README.md
Normal file
156
README.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# VConnect API Server
|
||||
|
||||
Zero-Port, Zero-VPN SSH 기반 Proxmox VM 원격접속 플랫폼
|
||||
|
||||
## 아키텍처
|
||||
|
||||
```
|
||||
[WPF Client] ←JWT→ [FastAPI Server] ←SSH→ [Proxmox VE]
|
||||
↓
|
||||
[PostgreSQL DB]
|
||||
```
|
||||
|
||||
## 핵심 기능
|
||||
|
||||
### 1. 인증 시스템
|
||||
- JWT 기반 토큰 인증
|
||||
- 사용자 관리 (회원가입/로그인)
|
||||
- 역할 기반 접근 제어 (RBAC)
|
||||
|
||||
### 2. Proxmox 통합
|
||||
- API Token 기반 Proxmox 연동
|
||||
- VM 목록 조회 및 상태 모니터링
|
||||
- VM 제어 (시작/종료/재시작)
|
||||
- QEMU Guest Agent IP 정보 수집
|
||||
|
||||
### 3. SSH 터널링 및 자격증명
|
||||
- **Zero-Trust Access**: 클라이언트는 SSH Private Key 없이 일회용 자격증명으로 접속
|
||||
- **임시 자격증명 발급**: JWT 토큰 인증 후 1시간 유효한 임시 SSH 비밀번호 발급
|
||||
- **PAM 연동**: SSH 게이트웨이에서 API를 통해 임시 비밀번호 검증
|
||||
- **동적 포트 할당**: 세션별 독립적인 포트 포워딩 관리
|
||||
|
||||
### 4. 감사 로그
|
||||
- 접속 기록 (누가, 언제, 어떤 VM에)
|
||||
- VM 제어 이력
|
||||
- 보안 이벤트 추적
|
||||
|
||||
### 5. 자동 업데이트
|
||||
- 클라이언트 버전 관리 및 업데이트 배포 지원
|
||||
|
||||
## 기술 스택
|
||||
|
||||
- **FastAPI**: 고성능 Python 웹 프레임워크
|
||||
- **PostgreSQL**: 관계형 데이터베이스
|
||||
- **SQLAlchemy**: ORM (Async support)
|
||||
- **Alembic**: DB 마이그레이션 관리
|
||||
- **JWT (Jose)**: 토큰 기반 인증
|
||||
- **Paramiko / AsyncSSH**: SSH 터널 및 연결 관리
|
||||
- **Uvicorn**: ASGI 웹 서버
|
||||
|
||||
## 프로젝트 구조
|
||||
|
||||
```
|
||||
vconnect-api/
|
||||
├── app/
|
||||
│ ├── main.py # FastAPI 앱 진입점
|
||||
│ ├── config.py # 설정 관리 (.env 로드)
|
||||
│ ├── database.py # DB 세션 및 엔진
|
||||
│ ├── models/ # SQLAlchemy 모델 (User, VM, AuditLog)
|
||||
│ ├── schemas/ # Pydantic 스키마 (Request/Response)
|
||||
│ ├── api/ # API 라우터
|
||||
│ │ ├── auth.py # 인증 (로그인/가입)
|
||||
│ │ ├── vms.py # VM 제어 및 조회
|
||||
│ │ ├── tunnel.py # SSH 터널 관리
|
||||
│ │ ├── ssh_credentials.py # 임시 SSH 자격증명 발급/검증
|
||||
│ │ └── admin.py # 관리자 기능 (사용자 관리, 로그)
|
||||
│ ├── services/ # 비즈니스 로직
|
||||
│ └── utils/ # 유틸리티 (JWT, Security)
|
||||
├── alembic/ # DB 마이그레이션 스크립트
|
||||
├── requirements.txt # 의존성 패키지 목록
|
||||
├── docker-compose.yml # Docker 배포 설정
|
||||
├── Dockerfile # Docker 빌드 설정
|
||||
└── README.md # 문서
|
||||
```
|
||||
|
||||
## 환경 변수 (.env)
|
||||
|
||||
```env
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:password@localhost/vconnect
|
||||
|
||||
# JWT
|
||||
JWT_SECRET_KEY=your-secret-key-change-it
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
|
||||
# Proxmox API
|
||||
PROXMOX_HOST=https://pve.example.com:8006
|
||||
PROXMOX_API_TOKEN=PVEAPIToken=root@pam!vconnect=xxx...
|
||||
|
||||
# SSH Gateway (Zero-Port)
|
||||
SSH_HOST=ssh.example.com # 클라이언트가 접속할 외부 주소
|
||||
SSH_PORT=22 # 외부 포트
|
||||
SSH_INTERNAL_HOST=127.0.0.1 # 내부 터널링 바인딩 주소
|
||||
```
|
||||
|
||||
## 설치 및 실행
|
||||
|
||||
```bash
|
||||
# 가상환경 생성
|
||||
python -m venv venv
|
||||
# Windows
|
||||
venv\Scripts\activate
|
||||
# Linux/Mac
|
||||
source venv/bin/activate
|
||||
|
||||
# 의존성 설치
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 환경 변수 설정
|
||||
cp .env.example .env
|
||||
# .env 파일 편집...
|
||||
|
||||
# DB 마이그레이션 (테이블 생성)
|
||||
alembic upgrade head
|
||||
|
||||
# 서버 실행 (개발 모드)
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
## API 엔드포인트 요약
|
||||
|
||||
### 인증 (`/api/v1/auth`)
|
||||
- `POST /register` - 회원가입
|
||||
- `POST /login` - 로그인 (AccessToken 발급)
|
||||
- `GET /me` - 현재 사용자 정보 확인
|
||||
|
||||
### VM 관리 (`/api/v1/vms`)
|
||||
- `GET /my` - 사용자별 할당된 VM 목록
|
||||
- `POST /{vm_id}/start` - VM 켜기
|
||||
- `POST /{vm_id}/stop` - VM 끄기
|
||||
|
||||
### SSH 자격증명 (`/api/v1/ssh`)
|
||||
- `POST /credentials` - [클라이언트용] 1시간 유효한 임시 SSH 비밀번호 발급
|
||||
- `POST /verify` - [게이트웨이용] 임시 비밀번호 유효성 검증
|
||||
|
||||
### 터널 관리 (`/api/v1/tunnel`)
|
||||
- `POST /create` - SSH 터널 세션 생성
|
||||
- `DELETE /{tunnel_id}` - 터널 세션 종료
|
||||
|
||||
### 관리자 (`/api/v1/admin`)
|
||||
- `GET /users` - 전체 사용자 관리
|
||||
- `GET /audit-logs` - 전체 감사 로그 조회
|
||||
- `GET /stats` - 시스템 통계
|
||||
|
||||
## 개발 로드맵 상태
|
||||
|
||||
- [x] 프로젝트 구조 및 FastAPI 기본 설정
|
||||
- [x] PostgreSQL 연동 및 Alembic 마이그레이션 구성
|
||||
- [x] JWT 인증 시스템 (Login/Refresh)
|
||||
- [x] Proxmox API 연동 (VM 상태/제어)
|
||||
- [x] SSH 터널링 매니저 구현
|
||||
- [x] **Zero-Trust 임시 자격증명 시스템** (SSH Password Manager)
|
||||
- [x] 감사 로그 (Audit Log)
|
||||
- [x] Docker 기반 배포 환경 (Dockerfile, Confirm)
|
||||
- [ ] 클라이언트 자동 업데이트 배포 시스템 고도화
|
||||
- [ ] 유닛 테스트 작성 (Pytest)
|
||||
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/database.cpython-312.pyc
Normal file
BIN
app/__pycache__/database.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main.cpython-312.pyc
Normal file
BIN
app/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/admin.cpython-312.pyc
Normal file
BIN
app/api/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/auth.cpython-312.pyc
Normal file
BIN
app/api/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/tunnel.cpython-312.pyc
Normal file
BIN
app/api/__pycache__/tunnel.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/vms.cpython-312.pyc
Normal file
BIN
app/api/__pycache__/vms.cpython-312.pyc
Normal file
Binary file not shown.
275
app/api/admin.py
Normal file
275
app/api/admin.py
Normal file
@@ -0,0 +1,275 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.database import get_db
|
||||
from app.schemas.admin import (
|
||||
UserCreate, UserUpdate, UserInfo, UserListResponse,
|
||||
VMAccessCreate, VMAccessUpdate, VMAccessInfo, VMAccessListResponse,
|
||||
AdminResponse
|
||||
)
|
||||
from app.schemas.auth import CurrentUser
|
||||
from app.api.auth import get_current_user
|
||||
from app.models.user import User, UserRole
|
||||
from app.models.vm import VMAccess
|
||||
from app.utils.security import hash_password
|
||||
from app.utils.exceptions import PermissionDeniedError, NotFoundError, BadRequestError
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def require_admin(current_user: CurrentUser = Depends(get_current_user)):
|
||||
"""관리자 권한 확인"""
|
||||
if current_user.role != "admin":
|
||||
raise PermissionDeniedError("관리자 권한이 필요합니다")
|
||||
return current_user
|
||||
|
||||
# ==================== 사용자 관리 ====================
|
||||
|
||||
@router.get("/users", response_model=UserListResponse)
|
||||
async def get_users(
|
||||
current_user: CurrentUser = Depends(require_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""사용자 목록 조회"""
|
||||
users = db.query(User).all()
|
||||
|
||||
user_list = [
|
||||
UserInfo(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
full_name=user.full_name,
|
||||
role=user.role.value,
|
||||
is_active=user.is_active,
|
||||
created_at=user.created_at,
|
||||
last_login=user.last_login
|
||||
)
|
||||
for user in users
|
||||
]
|
||||
|
||||
return UserListResponse(total=len(user_list), users=user_list)
|
||||
|
||||
@router.post("/users", response_model=AdminResponse)
|
||||
async def create_user(
|
||||
user_data: UserCreate,
|
||||
current_user: CurrentUser = Depends(require_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""사용자 생성"""
|
||||
# 중복 확인
|
||||
if db.query(User).filter(User.username == user_data.username).first():
|
||||
raise BadRequestError("이미 존재하는 사용자명입니다")
|
||||
|
||||
if db.query(User).filter(User.email == user_data.email).first():
|
||||
raise BadRequestError("이미 존재하는 이메일입니다")
|
||||
|
||||
# 역할 유효성 검사
|
||||
if user_data.role not in ["admin", "user"]:
|
||||
raise BadRequestError("역할은 'admin' 또는 'user'만 가능합니다")
|
||||
|
||||
# 사용자 생성
|
||||
new_user = User(
|
||||
username=user_data.username,
|
||||
email=user_data.email,
|
||||
hashed_password=hash_password(user_data.password),
|
||||
full_name=user_data.full_name,
|
||||
role=UserRole.ADMIN if user_data.role == "admin" else UserRole.USER,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
|
||||
return AdminResponse(
|
||||
success=True,
|
||||
message=f"사용자 '{user_data.username}'이(가) 생성되었습니다"
|
||||
)
|
||||
|
||||
@router.put("/users/{user_id}", response_model=AdminResponse)
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
user_data: UserUpdate,
|
||||
current_user: CurrentUser = Depends(require_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""사용자 정보 수정"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise NotFoundError("사용자를 찾을 수 없습니다")
|
||||
|
||||
# 이메일 중복 확인
|
||||
if user_data.email and user_data.email != user.email:
|
||||
if db.query(User).filter(User.email == user_data.email).first():
|
||||
raise BadRequestError("이미 존재하는 이메일입니다")
|
||||
user.email = user_data.email
|
||||
|
||||
# 비밀번호 변경
|
||||
if user_data.password:
|
||||
user.hashed_password = hash_password(user_data.password)
|
||||
|
||||
# 기타 정보 업데이트
|
||||
if user_data.full_name is not None:
|
||||
user.full_name = user_data.full_name
|
||||
|
||||
if user_data.role:
|
||||
if user_data.role not in ["admin", "user"]:
|
||||
raise BadRequestError("역할은 'admin' 또는 'user'만 가능합니다")
|
||||
user.role = UserRole.ADMIN if user_data.role == "admin" else UserRole.USER
|
||||
|
||||
if user_data.is_active is not None:
|
||||
user.is_active = user_data.is_active
|
||||
|
||||
db.commit()
|
||||
|
||||
return AdminResponse(
|
||||
success=True,
|
||||
message=f"사용자 '{user.username}'의 정보가 수정되었습니다"
|
||||
)
|
||||
|
||||
@router.delete("/users/{user_id}", response_model=AdminResponse)
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
current_user: CurrentUser = Depends(require_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""사용자 삭제"""
|
||||
# 자기 자신은 삭제 불가
|
||||
if user_id == current_user.id:
|
||||
raise BadRequestError("자기 자신은 삭제할 수 없습니다")
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise NotFoundError("사용자를 찾을 수 없습니다")
|
||||
|
||||
username = user.username
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
|
||||
return AdminResponse(
|
||||
success=True,
|
||||
message=f"사용자 '{username}'이(가) 삭제되었습니다"
|
||||
)
|
||||
|
||||
# ==================== VM 접근 권한 관리 ====================
|
||||
|
||||
@router.get("/vm-access", response_model=VMAccessListResponse)
|
||||
async def get_vm_access_list(
|
||||
current_user: CurrentUser = Depends(require_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""VM 접근 권한 목록 조회"""
|
||||
accesses = db.query(VMAccess).join(User).all()
|
||||
|
||||
access_list = [
|
||||
VMAccessInfo(
|
||||
id=access.id,
|
||||
user_id=access.user_id,
|
||||
username=access.user.username,
|
||||
vm_id=access.vm_id,
|
||||
node=access.node,
|
||||
vm_name=access.vm_name,
|
||||
static_ip=access.static_ip,
|
||||
rdp_username=access.rdp_username,
|
||||
rdp_port=access.rdp_port,
|
||||
is_active=access.is_active,
|
||||
created_at=access.created_at
|
||||
)
|
||||
for access in accesses
|
||||
]
|
||||
|
||||
return VMAccessListResponse(total=len(access_list), accesses=access_list)
|
||||
|
||||
@router.post("/vm-access", response_model=AdminResponse)
|
||||
async def create_vm_access(
|
||||
access_data: VMAccessCreate,
|
||||
current_user: CurrentUser = Depends(require_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""VM 접근 권한 부여"""
|
||||
# 사용자 존재 확인
|
||||
user = db.query(User).filter(User.id == access_data.user_id).first()
|
||||
if not user:
|
||||
raise NotFoundError("사용자를 찾을 수 없습니다")
|
||||
|
||||
# 중복 확인
|
||||
existing = db.query(VMAccess).filter(
|
||||
VMAccess.user_id == access_data.user_id,
|
||||
VMAccess.vm_id == access_data.vm_id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise BadRequestError("이미 해당 사용자에게 VM 접근 권한이 있습니다")
|
||||
|
||||
# 권한 생성
|
||||
access = VMAccess(
|
||||
user_id=access_data.user_id,
|
||||
vm_id=access_data.vm_id,
|
||||
node=access_data.node,
|
||||
static_ip=access_data.static_ip,
|
||||
rdp_username=access_data.rdp_username,
|
||||
rdp_password=access_data.rdp_password,
|
||||
rdp_port=access_data.rdp_port,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
db.add(access)
|
||||
db.commit()
|
||||
|
||||
return AdminResponse(
|
||||
success=True,
|
||||
message=f"사용자 '{user.username}'에게 VM {access_data.vm_id} 접근 권한이 부여되었습니다"
|
||||
)
|
||||
|
||||
@router.put("/vm-access/{access_id}", response_model=AdminResponse)
|
||||
async def update_vm_access(
|
||||
access_id: int,
|
||||
access_data: VMAccessUpdate,
|
||||
current_user: CurrentUser = Depends(require_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""VM 접근 권한 수정"""
|
||||
access = db.query(VMAccess).filter(VMAccess.id == access_id).first()
|
||||
if not access:
|
||||
raise NotFoundError("접근 권한을 찾을 수 없습니다")
|
||||
|
||||
# 정보 업데이트
|
||||
if access_data.static_ip is not None:
|
||||
access.static_ip = access_data.static_ip
|
||||
|
||||
if access_data.rdp_username is not None:
|
||||
access.rdp_username = access_data.rdp_username
|
||||
|
||||
if access_data.rdp_password is not None:
|
||||
access.rdp_password = access_data.rdp_password
|
||||
|
||||
if access_data.rdp_port is not None:
|
||||
access.rdp_port = access_data.rdp_port
|
||||
|
||||
if access_data.is_active is not None:
|
||||
access.is_active = access_data.is_active
|
||||
|
||||
db.commit()
|
||||
|
||||
return AdminResponse(
|
||||
success=True,
|
||||
message=f"VM {access.vm_id} 접근 권한이 수정되었습니다"
|
||||
)
|
||||
|
||||
@router.delete("/vm-access/{access_id}", response_model=AdminResponse)
|
||||
async def delete_vm_access(
|
||||
access_id: int,
|
||||
current_user: CurrentUser = Depends(require_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""VM 접근 권한 삭제"""
|
||||
access = db.query(VMAccess).filter(VMAccess.id == access_id).first()
|
||||
if not access:
|
||||
raise NotFoundError("접근 권한을 찾을 수 없습니다")
|
||||
|
||||
vm_id = access.vm_id
|
||||
db.delete(access)
|
||||
db.commit()
|
||||
|
||||
return AdminResponse(
|
||||
success=True,
|
||||
message=f"VM {vm_id} 접근 권한이 삭제되었습니다"
|
||||
)
|
||||
101
app/api/auth.py
Normal file
101
app/api/auth.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.schemas.auth import UserRegister, UserLogin, Token, UserResponse, CurrentUser
|
||||
from app.services.auth_service import auth_service
|
||||
from app.utils.jwt_handler import decode_token
|
||||
from app.utils.exceptions import AuthenticationError
|
||||
|
||||
router = APIRouter()
|
||||
security = HTTPBearer()
|
||||
|
||||
# 현재 사용자 가져오기 의존성
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
) -> CurrentUser:
|
||||
"""JWT 토큰에서 현재 사용자 정보 추출"""
|
||||
token = credentials.credentials
|
||||
payload = decode_token(token)
|
||||
|
||||
if not payload:
|
||||
raise AuthenticationError("유효하지 않은 토큰입니다")
|
||||
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise AuthenticationError("토큰 정보가 올바르지 않습니다")
|
||||
|
||||
user = auth_service.get_user_by_id(db, int(user_id))
|
||||
if not user:
|
||||
raise AuthenticationError("사용자를 찾을 수 없습니다")
|
||||
|
||||
if not user.is_active:
|
||||
raise AuthenticationError("비활성화된 계정입니다")
|
||||
|
||||
return CurrentUser(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
role=user.role,
|
||||
is_active=user.is_active
|
||||
)
|
||||
|
||||
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def register(user_data: UserRegister, db: Session = Depends(get_db)):
|
||||
"""
|
||||
회원가입
|
||||
|
||||
새로운 사용자 계정을 생성합니다.
|
||||
"""
|
||||
user = auth_service.register_user(db, user_data)
|
||||
return user
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(login_data: UserLogin, db: Session = Depends(get_db)):
|
||||
"""
|
||||
로그인
|
||||
|
||||
사용자 인증 후 JWT 토큰을 발급합니다.
|
||||
"""
|
||||
user = auth_service.authenticate_user(db, login_data.username, login_data.password)
|
||||
tokens = auth_service.create_tokens(user)
|
||||
return tokens
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_me(
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
현재 사용자 정보 조회
|
||||
|
||||
JWT 토큰을 기반으로 현재 로그인한 사용자 정보를 반환합니다.
|
||||
"""
|
||||
user = auth_service.get_user_by_id(db, current_user.id)
|
||||
return user
|
||||
|
||||
@router.post("/refresh", response_model=Token)
|
||||
async def refresh_token(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
토큰 갱신
|
||||
|
||||
Refresh Token을 사용하여 새로운 Access Token을 발급합니다.
|
||||
"""
|
||||
token = credentials.credentials
|
||||
payload = decode_token(token)
|
||||
|
||||
if not payload or payload.get("type") != "refresh":
|
||||
raise AuthenticationError("유효하지 않은 Refresh Token입니다")
|
||||
|
||||
user_id = payload.get("sub")
|
||||
user = auth_service.get_user_by_id(db, int(user_id))
|
||||
|
||||
if not user or not user.is_active:
|
||||
raise AuthenticationError("사용자를 찾을 수 없습니다")
|
||||
|
||||
tokens = auth_service.create_tokens(user)
|
||||
return tokens
|
||||
66
app/api/ssh_credentials.py
Normal file
66
app/api/ssh_credentials.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, timedelta
|
||||
from app.api.auth import get_current_user
|
||||
from app.schemas.auth import CurrentUser
|
||||
from app.services.temp_ssh_password_service import temp_ssh_password_manager
|
||||
import os
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class SshVerifyRequest(BaseModel):
|
||||
"""SSH 비밀번호 검증 요청"""
|
||||
username: str
|
||||
password: str
|
||||
|
||||
@router.post("/credentials")
|
||||
async def get_ssh_credentials(current_user: CurrentUser = Depends(get_current_user)):
|
||||
"""
|
||||
JWT 토큰으로 임시 SSH 자격증명 발급
|
||||
|
||||
Returns:
|
||||
SSH 연결 정보 및 임시 비밀번호
|
||||
"""
|
||||
# 임시 비밀번호 생성 (1시간 유효)
|
||||
temp_password = temp_ssh_password_manager.generate_password(
|
||||
username=current_user.username,
|
||||
validity_hours=1
|
||||
)
|
||||
|
||||
# SSH 서버 정보 (외부 접속용)
|
||||
ssh_host = os.getenv("SSH_HOST", "api.mouse84.com") # 외부 DDNS
|
||||
ssh_port = int(os.getenv("SSH_PORT", "54054")) # 외부 포트 (내부 22로 포워딩)
|
||||
|
||||
# 만료 시간 계산
|
||||
expires_at = datetime.utcnow() + timedelta(hours=1)
|
||||
|
||||
return {
|
||||
"ssh_host": ssh_host,
|
||||
"ssh_port": ssh_port,
|
||||
"ssh_username": current_user.username,
|
||||
"ssh_password": temp_password,
|
||||
"expires_at": expires_at.isoformat(),
|
||||
"expires_in_seconds": 3600
|
||||
}
|
||||
|
||||
@router.post("/verify")
|
||||
async def verify_ssh_password(request: SshVerifyRequest):
|
||||
"""
|
||||
임시 SSH 비밀번호 검증 (PAM 인증용)
|
||||
|
||||
Args:
|
||||
request: 사용자명과 비밀번호
|
||||
|
||||
Returns:
|
||||
200: 비밀번호 유효
|
||||
401: 비밀번호 무효
|
||||
"""
|
||||
is_valid = temp_ssh_password_manager.verify_password(
|
||||
username=request.username,
|
||||
password=request.password
|
||||
)
|
||||
|
||||
if is_valid:
|
||||
return {"valid": True, "message": "Password verified"}
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail="Invalid password")
|
||||
127
app/api/tunnel.py
Normal file
127
app/api/tunnel.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.schemas.tunnel import (
|
||||
TunnelCreateRequest,
|
||||
TunnelCreateResponse,
|
||||
TunnelStatusResponse,
|
||||
TunnelCloseResponse,
|
||||
TunnelInfo
|
||||
)
|
||||
from app.schemas.auth import CurrentUser
|
||||
from app.api.auth import get_current_user
|
||||
from app.services.ssh_tunnel_service import ssh_tunnel_manager
|
||||
from app.services.proxmox_service import proxmox_service
|
||||
from app.models.vm import VMAccess
|
||||
from app.utils.exceptions import NotFoundError, BadRequestError
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/create", response_model=TunnelCreateResponse)
|
||||
async def create_tunnel(
|
||||
request: TunnelCreateRequest,
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
SSH 터널 생성
|
||||
|
||||
VM에 접속하기 위한 SSH Local Port Forwarding 터널을 생성합니다.
|
||||
클라이언트는 반환된 local_port로 RDP 연결을 시도해야 합니다.
|
||||
"""
|
||||
# VM IP 주소 확인
|
||||
# 1. 요청에 IP가 포함되어 있으면 사용 (Guest Agent 없이도 가능)
|
||||
ip_address = request.vm_ip
|
||||
|
||||
# 2. 없으면 Guest Agent로 조회
|
||||
if not ip_address:
|
||||
try:
|
||||
ip_address = await proxmox_service.get_vm_ip(request.node, request.vm_id)
|
||||
if ip_address:
|
||||
print(f"✅ Guest Agent에서 IP 조회 성공: {ip_address}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Guest Agent IP 조회 실패: {str(e)}")
|
||||
ip_address = None
|
||||
|
||||
# 3. Guest Agent 실패 시 VMAccess 테이블의 static_ip 사용
|
||||
if not ip_address:
|
||||
vm_access = db.query(VMAccess).filter(
|
||||
VMAccess.user_id == current_user.id,
|
||||
VMAccess.vm_id == request.vm_id,
|
||||
VMAccess.node == request.node
|
||||
).first()
|
||||
|
||||
if vm_access and vm_access.static_ip:
|
||||
ip_address = vm_access.static_ip
|
||||
print(f"✅ Static IP 사용: {ip_address}")
|
||||
else:
|
||||
raise BadRequestError(
|
||||
"VM의 IP 주소를 확인할 수 없습니다. "
|
||||
"관리자 패널에서 고정 IP를 설정하거나 QEMU Guest Agent를 설치하세요."
|
||||
)
|
||||
|
||||
try:
|
||||
# SSH 터널 생성
|
||||
tunnel_info = await ssh_tunnel_manager.create_tunnel(
|
||||
user_id=current_user.id,
|
||||
vm_id=request.vm_id,
|
||||
remote_host=ip_address,
|
||||
remote_port=3389 # RDP 포트
|
||||
)
|
||||
|
||||
return TunnelCreateResponse(
|
||||
success=True,
|
||||
message="SSH 터널이 생성되었습니다",
|
||||
session_id=tunnel_info["session_id"],
|
||||
tunnel_info=TunnelInfo(
|
||||
session_id=tunnel_info["session_id"],
|
||||
local_port=tunnel_info["local_port"],
|
||||
remote_host=tunnel_info["remote_host"],
|
||||
remote_port=tunnel_info["remote_port"],
|
||||
vm_id=request.vm_id,
|
||||
is_active=True,
|
||||
created_at=None # 자동 설정
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
return TunnelCreateResponse(
|
||||
success=False,
|
||||
message=f"터널 생성 실패: {str(e)}",
|
||||
session_id="",
|
||||
tunnel_info=None
|
||||
)
|
||||
|
||||
@router.get("/{session_id}/status", response_model=TunnelStatusResponse)
|
||||
async def get_tunnel_status(
|
||||
session_id: str,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
터널 상태 조회
|
||||
|
||||
활성 터널의 상태를 확인합니다.
|
||||
"""
|
||||
status = await ssh_tunnel_manager.get_tunnel_status(session_id)
|
||||
|
||||
if not status:
|
||||
raise NotFoundError("터널을 찾을 수 없습니다")
|
||||
|
||||
return TunnelStatusResponse(**status)
|
||||
|
||||
@router.delete("/{session_id}", response_model=TunnelCloseResponse)
|
||||
async def close_tunnel(
|
||||
session_id: str,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
터널 종료
|
||||
|
||||
SSH 터널을 종료하고 리소스를 정리합니다.
|
||||
"""
|
||||
success = await ssh_tunnel_manager.close_tunnel(session_id)
|
||||
|
||||
return TunnelCloseResponse(
|
||||
success=success,
|
||||
message="터널이 종료되었습니다" if success else "터널 종료에 실패했습니다",
|
||||
session_id=session_id
|
||||
)
|
||||
141
app/api/vms.py
Normal file
141
app/api/vms.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.database import get_db
|
||||
from app.schemas.vm import VMInfo, VMListResponse, VMDetail, VMControlRequest, VMControlResponse
|
||||
from app.schemas.auth import CurrentUser
|
||||
from app.api.auth import get_current_user
|
||||
from app.services.proxmox_service import proxmox_service
|
||||
from app.models.vm import VMAccess
|
||||
from app.utils.exceptions import NotFoundError, PermissionDeniedError
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/my", response_model=VMListResponse)
|
||||
async def get_my_vms(
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
내 VM 목록 조회
|
||||
|
||||
현재 사용자가 접근 가능한 VM 목록을 반환합니다.
|
||||
"""
|
||||
# Proxmox에서 모든 VM 가져오기
|
||||
all_vms = await proxmox_service.get_all_vms()
|
||||
|
||||
# 현재 사용자의 VMAccess 정보 조회
|
||||
vm_accesses = db.query(VMAccess).filter(
|
||||
VMAccess.user_id == current_user.id,
|
||||
VMAccess.is_active == True
|
||||
).all()
|
||||
|
||||
# vm_id를 키로 하는 딕셔너리 생성
|
||||
access_map = {access.vm_id: access for access in vm_accesses}
|
||||
|
||||
vm_list = []
|
||||
for vm in all_vms:
|
||||
vm_id = vm["vmid"]
|
||||
access = access_map.get(vm_id)
|
||||
|
||||
# VMAccess가 있으면 해당 정보 사용, 없으면 기본값
|
||||
vm_info = VMInfo(
|
||||
vm_id=vm_id,
|
||||
node=vm["node"],
|
||||
name=vm.get("name", "Unknown"),
|
||||
status=vm.get("status", "unknown"),
|
||||
ip_address=access.static_ip if access else None, # Static IP 자동 설정
|
||||
cpus=vm.get("cpus", 0),
|
||||
memory=vm.get("maxmem", 0) // (1024 * 1024), # bytes to MB
|
||||
memory_usage=vm.get("mem", 0) // (1024 * 1024) if vm.get("mem") else None,
|
||||
cpu_usage=vm.get("cpu", 0),
|
||||
can_start=True,
|
||||
can_stop=True,
|
||||
can_reboot=True,
|
||||
can_connect=True,
|
||||
rdp_username=access.rdp_username if access else None, # RDP 사용자명
|
||||
rdp_password=access.rdp_password if access else None, # RDP 비밀번호
|
||||
rdp_port=access.rdp_port if access else 3389 # RDP 포트
|
||||
)
|
||||
vm_list.append(vm_info)
|
||||
|
||||
return VMListResponse(total=len(vm_list), vms=vm_list)
|
||||
|
||||
@router.get("/{vm_id}", response_model=VMDetail)
|
||||
async def get_vm_detail(
|
||||
vm_id: int,
|
||||
node: str,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
VM 상세 정보 조회
|
||||
"""
|
||||
# VM 상태 조회
|
||||
status = await proxmox_service.get_vm_status(node, vm_id)
|
||||
|
||||
if not status:
|
||||
raise NotFoundError(f"VM {vm_id}를 찾을 수 없습니다")
|
||||
|
||||
# IP 조회 제거 - 연결에 필요하지 않음
|
||||
return VMDetail(
|
||||
vm_id=vm_id,
|
||||
node=node,
|
||||
name=status.get("name", "Unknown"),
|
||||
status=status.get("status", "unknown"),
|
||||
ip_address=None, # IP 조회 안 함
|
||||
cpus=status.get("cpus", 0),
|
||||
memory=status.get("maxmem", 0) // (1024 * 1024),
|
||||
memory_usage=status.get("mem", 0) // (1024 * 1024) if status.get("mem") else None,
|
||||
cpu_usage=status.get("cpu", 0),
|
||||
uptime=status.get("uptime"),
|
||||
rdp_port=3389,
|
||||
has_guest_agent=False # Guest Agent 불필요
|
||||
)
|
||||
|
||||
@router.post("/{vm_id}/start", response_model=VMControlResponse)
|
||||
async def start_vm(
|
||||
vm_id: int,
|
||||
node: str,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""VM 시작"""
|
||||
success = await proxmox_service.start_vm(node, vm_id)
|
||||
|
||||
return VMControlResponse(
|
||||
success=success,
|
||||
message="VM이 시작되었습니다" if success else "VM 시작에 실패했습니다",
|
||||
vm_id=vm_id,
|
||||
action="start"
|
||||
)
|
||||
|
||||
@router.post("/{vm_id}/stop", response_model=VMControlResponse)
|
||||
async def stop_vm(
|
||||
vm_id: int,
|
||||
node: str,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""VM 종료"""
|
||||
success = await proxmox_service.stop_vm(node, vm_id)
|
||||
|
||||
return VMControlResponse(
|
||||
success=success,
|
||||
message="VM이 종료되었습니다" if success else "VM 종료에 실패했습니다",
|
||||
vm_id=vm_id,
|
||||
action="stop"
|
||||
)
|
||||
|
||||
@router.post("/{vm_id}/reboot", response_model=VMControlResponse)
|
||||
async def reboot_vm(
|
||||
vm_id: int,
|
||||
node: str,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""VM 재시작"""
|
||||
success = await proxmox_service.reboot_vm(node, vm_id)
|
||||
|
||||
return VMControlResponse(
|
||||
success=success,
|
||||
message="VM이 재시작되었습니다" if success else "VM 재시작에 실패했습니다",
|
||||
vm_id=vm_id,
|
||||
action="reboot"
|
||||
)
|
||||
53
app/config.py
Normal file
53
app/config.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import List
|
||||
import os
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# Application
|
||||
APP_NAME: str = "VConnect API"
|
||||
APP_VERSION: str = "1.0.0"
|
||||
DEBUG: bool = True
|
||||
API_V1_PREFIX: str = "/api"
|
||||
|
||||
# Database
|
||||
DATABASE_URL: str = "sqlite:///./vconnect.db"
|
||||
|
||||
# JWT
|
||||
JWT_SECRET_KEY: str = "your-secret-key-change-in-production"
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
||||
|
||||
# Proxmox
|
||||
PROXMOX_HOST: str = "https://pve.mouse84.com:8006"
|
||||
PROXMOX_API_TOKEN: str = ""
|
||||
PROXMOX_VERIFY_SSL: bool = False
|
||||
|
||||
# SSH Gateway
|
||||
SSH_HOST: str = ""
|
||||
SSH_PORT: int = 22
|
||||
SSH_USERNAME: str = ""
|
||||
SSH_KEY_PATH: str = ""
|
||||
SSH_PASSWORD: str = ""
|
||||
|
||||
# Tunnel Port Range
|
||||
TUNNEL_PORT_MIN: int = 50000
|
||||
TUNNEL_PORT_MAX: int = 60000
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS: List[str] = ["http://localhost:8080", "http://localhost:3000"]
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL: str = "INFO"
|
||||
LOG_FILE: str = "logs/vconnect.log"
|
||||
|
||||
# Admin
|
||||
ADMIN_USERNAME: str = "admin"
|
||||
ADMIN_PASSWORD: str = "admin123"
|
||||
ADMIN_EMAIL: str = "admin@example.com"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
settings = Settings()
|
||||
25
app/database.py
Normal file
25
app/database.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.config import settings
|
||||
|
||||
# SQLAlchemy 엔진 생성
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {},
|
||||
echo=settings.DEBUG
|
||||
)
|
||||
|
||||
# 세션 팩토리
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# Base 클래스
|
||||
Base = declarative_base()
|
||||
|
||||
# DB 세션 의존성
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
94
app/main.py
Normal file
94
app/main.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
from app.config import settings
|
||||
from app.database import engine, Base
|
||||
from app.api import auth, vms, tunnel, admin, ssh_credentials
|
||||
import logging
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
level=settings.LOG_LEVEL,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 라이프사이클 이벤트
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# 시작 시
|
||||
logger.info("🚀 VConnect API 서버 시작")
|
||||
|
||||
# DB 테이블 생성
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# 관리자 계정 생성 (없으면)
|
||||
from app.services.auth_service import auth_service
|
||||
from app.database import SessionLocal
|
||||
from app.schemas.auth import UserRegister
|
||||
from app.models.user import UserRole
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
existing_admin = auth_service.get_user_by_username(db, settings.ADMIN_USERNAME)
|
||||
if not existing_admin:
|
||||
admin_data = UserRegister(
|
||||
username=settings.ADMIN_USERNAME,
|
||||
email=settings.ADMIN_EMAIL,
|
||||
password=settings.ADMIN_PASSWORD,
|
||||
full_name="Administrator"
|
||||
)
|
||||
admin_user = auth_service.register_user(db, admin_data)
|
||||
admin_user.role = UserRole.ADMIN
|
||||
db.commit()
|
||||
logger.info(f"✅ 관리자 계정 생성: {settings.ADMIN_USERNAME}")
|
||||
except Exception as e:
|
||||
logger.error(f"관리자 계정 생성 실패: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
yield
|
||||
|
||||
# 종료 시
|
||||
logger.info("🛑 VConnect API 서버 종료")
|
||||
|
||||
# FastAPI 앱 생성
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.APP_VERSION,
|
||||
description="Zero-Port, Zero-VPN SSH 기반 Proxmox VM 원격접속 플랫폼",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS 설정
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# API 라우터 등록
|
||||
app.include_router(auth.router, prefix=f"{settings.API_V1_PREFIX}/auth", tags=["인증"])
|
||||
app.include_router(vms.router, prefix=f"{settings.API_V1_PREFIX}/vms", tags=["VM 관리"])
|
||||
app.include_router(tunnel.router, prefix=f"{settings.API_V1_PREFIX}/tunnel", tags=["터널 관리"])
|
||||
app.include_router(admin.router, prefix=f"{settings.API_V1_PREFIX}/admin", tags=["관리자"])
|
||||
app.include_router(ssh_credentials.router, prefix=f"{settings.API_V1_PREFIX}/ssh", tags=["SSH 자격증명"])
|
||||
|
||||
# Health Check
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {
|
||||
"status": "healthy",
|
||||
"app_name": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION
|
||||
}
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "VConnect API Server",
|
||||
"version": settings.APP_VERSION,
|
||||
"docs": "/docs"
|
||||
}
|
||||
BIN
app/models/__pycache__/user.cpython-312.pyc
Normal file
BIN
app/models/__pycache__/user.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/models/__pycache__/vm.cpython-312.pyc
Normal file
BIN
app/models/__pycache__/vm.cpython-312.pyc
Normal file
Binary file not shown.
43
app/models/audit_log.py
Normal file
43
app/models/audit_log.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, Enum
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
import enum
|
||||
|
||||
class AuditAction(str, enum.Enum):
|
||||
LOGIN = "login"
|
||||
LOGOUT = "logout"
|
||||
VM_CONNECT = "vm_connect"
|
||||
VM_DISCONNECT = "vm_disconnect"
|
||||
VM_START = "vm_start"
|
||||
VM_STOP = "vm_stop"
|
||||
VM_REBOOT = "vm_reboot"
|
||||
TUNNEL_CREATE = "tunnel_create"
|
||||
TUNNEL_CLOSE = "tunnel_close"
|
||||
USER_CREATE = "user_create"
|
||||
USER_UPDATE = "user_update"
|
||||
USER_DELETE = "user_delete"
|
||||
ACCESS_DENIED = "access_denied"
|
||||
|
||||
class AuditLog(Base):
|
||||
"""감사 로그 - 모든 중요 작업 기록"""
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
username = Column(String(50)) # 비정규화 (삭제된 사용자 추적)
|
||||
|
||||
action = Column(Enum(AuditAction), nullable=False, index=True)
|
||||
resource_type = Column(String(50)) # "vm", "user", "tunnel"
|
||||
resource_id = Column(String(100)) # VM ID, User ID 등
|
||||
|
||||
ip_address = Column(String(50))
|
||||
user_agent = Column(String(255))
|
||||
|
||||
details = Column(Text) # JSON 형태로 추가 정보 저장
|
||||
success = Column(Integer, default=True)
|
||||
error_message = Column(Text)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<AuditLog(user='{self.username}', action='{self.action}', created_at='{self.created_at}')>"
|
||||
28
app/models/user.py
Normal file
28
app/models/user.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Enum
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.database import Base
|
||||
import enum
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
ADMIN = "admin"
|
||||
USER = "user"
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, index=True, nullable=False)
|
||||
email = Column(String(100), unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String(255), nullable=False)
|
||||
full_name = Column(String(100))
|
||||
role = Column(Enum(UserRole), default=UserRole.USER, nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
last_login = Column(DateTime(timezone=True))
|
||||
|
||||
vm_accesses = relationship("VMAccess", back_populates="user")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(id={self.id}, username='{self.username}', role='{self.role}')>"
|
||||
62
app/models/vm.py
Normal file
62
app/models/vm.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.database import Base
|
||||
|
||||
class VMAccess(Base):
|
||||
"""사용자별 VM 접근 권한"""
|
||||
__tablename__ = "vm_access"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
vm_id = Column(Integer, nullable=False) # Proxmox VM ID
|
||||
node = Column(String(50), nullable=False) # Proxmox 노드명
|
||||
vm_name = Column(String(100))
|
||||
|
||||
# RDP 접속 정보
|
||||
rdp_username = Column(String(50))
|
||||
rdp_password = Column(String(255)) # 암호화 저장
|
||||
rdp_port = Column(Integer, default=3389)
|
||||
|
||||
# 권한
|
||||
can_start = Column(Boolean, default=True)
|
||||
can_stop = Column(Boolean, default=True)
|
||||
can_reboot = Column(Boolean, default=True)
|
||||
can_connect = Column(Boolean, default=True)
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Guest Agent 없을 때 사용할 고정 IP
|
||||
static_ip = Column(String(50), nullable=True)
|
||||
|
||||
# 관계
|
||||
user = relationship("User", back_populates="vm_accesses")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VMAccess(user_id={self.user_id}, vm_id={self.vm_id}, node='{self.node}')>"
|
||||
|
||||
|
||||
class SSHTunnel(Base):
|
||||
"""활성 SSH 터널 세션"""
|
||||
__tablename__ = "ssh_tunnels"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
vm_id = Column(Integer, nullable=False)
|
||||
|
||||
# 터널 정보
|
||||
local_port = Column(Integer, nullable=False) # 클라이언트가 사용할 포트
|
||||
remote_host = Column(String(50), nullable=False) # VM IP
|
||||
remote_port = Column(Integer, nullable=False) # VM RDP 포트
|
||||
|
||||
# 세션 정보
|
||||
session_id = Column(String(100), unique=True, index=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
closed_at = Column(DateTime(timezone=True))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SSHTunnel(session_id='{self.session_id}', local_port={self.local_port})>"
|
||||
BIN
app/schemas/__pycache__/admin.cpython-312.pyc
Normal file
BIN
app/schemas/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
BIN
app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/schemas/__pycache__/tunnel.cpython-312.pyc
Normal file
BIN
app/schemas/__pycache__/tunnel.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/schemas/__pycache__/vm.cpython-312.pyc
Normal file
BIN
app/schemas/__pycache__/vm.cpython-312.pyc
Normal file
Binary file not shown.
71
app/schemas/admin.py
Normal file
71
app/schemas/admin.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
# 사용자 관리
|
||||
class UserCreate(BaseModel):
|
||||
username: str
|
||||
email: EmailStr
|
||||
password: str
|
||||
full_name: Optional[str] = None
|
||||
role: str = "user" # admin or user
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
email: Optional[EmailStr] = None
|
||||
password: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
full_name: Optional[str]
|
||||
role: str
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
last_login: Optional[datetime]
|
||||
|
||||
class UserListResponse(BaseModel):
|
||||
total: int
|
||||
users: List[UserInfo]
|
||||
|
||||
# VM 접근 권한 관리
|
||||
class VMAccessCreate(BaseModel):
|
||||
user_id: int
|
||||
vm_id: int
|
||||
node: str
|
||||
static_ip: Optional[str] = None
|
||||
rdp_username: Optional[str] = None
|
||||
rdp_password: Optional[str] = None
|
||||
rdp_port: int = 3389
|
||||
|
||||
class VMAccessUpdate(BaseModel):
|
||||
static_ip: Optional[str] = None
|
||||
rdp_username: Optional[str] = None
|
||||
rdp_password: Optional[str] = None
|
||||
rdp_port: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class VMAccessInfo(BaseModel):
|
||||
id: int
|
||||
user_id: int
|
||||
username: str # 조인해서 가져옴
|
||||
vm_id: int
|
||||
node: str
|
||||
vm_name: Optional[str]
|
||||
static_ip: Optional[str]
|
||||
rdp_username: Optional[str]
|
||||
rdp_port: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
class VMAccessListResponse(BaseModel):
|
||||
total: int
|
||||
accesses: List[VMAccessInfo]
|
||||
|
||||
# 공통 응답
|
||||
class AdminResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
51
app/schemas/auth.py
Normal file
51
app/schemas/auth.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from app.models.user import UserRole
|
||||
|
||||
# 회원가입 요청
|
||||
class UserRegister(BaseModel):
|
||||
username: str = Field(..., min_length=3, max_length=50)
|
||||
email: EmailStr
|
||||
password: str = Field(..., min_length=6)
|
||||
full_name: Optional[str] = None
|
||||
|
||||
# 로그인 요청
|
||||
class UserLogin(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
# 토큰 응답
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int
|
||||
|
||||
# 토큰 페이로드
|
||||
class TokenPayload(BaseModel):
|
||||
sub: str # user_id
|
||||
exp: datetime
|
||||
role: UserRole
|
||||
|
||||
# 사용자 응답
|
||||
class UserResponse(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
full_name: Optional[str]
|
||||
role: UserRole
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
last_login: Optional[datetime]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# 현재 사용자 정보
|
||||
class CurrentUser(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
role: UserRole
|
||||
is_active: bool
|
||||
44
app/schemas/tunnel.py
Normal file
44
app/schemas/tunnel.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
# 터널 생성 요청
|
||||
class TunnelCreateRequest(BaseModel):
|
||||
vm_id: int
|
||||
node: str
|
||||
vm_ip: Optional[str] = None # Guest Agent 없이 수동으로 IP 지정 가능
|
||||
|
||||
# 터널 생성 응답
|
||||
class TunnelCreateResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
session_id: str
|
||||
tunnel_info: Optional['TunnelInfo'] = None
|
||||
|
||||
# 터널 정보
|
||||
class TunnelInfo(BaseModel):
|
||||
session_id: str
|
||||
local_port: int
|
||||
remote_host: str
|
||||
remote_port: int
|
||||
vm_id: int
|
||||
vm_name: Optional[str] = None
|
||||
rdp_username: Optional[str] = None
|
||||
is_active: bool
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# 터널 상태 응답
|
||||
class TunnelStatusResponse(BaseModel):
|
||||
session_id: str
|
||||
is_active: bool
|
||||
uptime_seconds: Optional[int] = None
|
||||
created_at: datetime
|
||||
|
||||
# 터널 종료 응답
|
||||
class TunnelCloseResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
session_id: str
|
||||
52
app/schemas/vm.py
Normal file
52
app/schemas/vm.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
# VM 정보 응답
|
||||
class VMInfo(BaseModel):
|
||||
vm_id: int
|
||||
node: str
|
||||
name: str
|
||||
status: str
|
||||
ip_address: Optional[str] = None
|
||||
cpus: int
|
||||
memory: int # MB
|
||||
memory_usage: Optional[int] = None
|
||||
cpu_usage: Optional[float] = None
|
||||
uptime: Optional[int] = None
|
||||
|
||||
# 접근 권한 정보
|
||||
can_start: bool = True
|
||||
can_stop: bool = True
|
||||
can_reboot: bool = True
|
||||
can_connect: bool = True
|
||||
|
||||
# RDP 연결 정보 (VMAccess에서 가져옴)
|
||||
rdp_username: Optional[str] = None
|
||||
rdp_password: Optional[str] = None
|
||||
rdp_port: int = 3389
|
||||
|
||||
# VM 목록 응답
|
||||
class VMListResponse(BaseModel):
|
||||
total: int
|
||||
vms: list[VMInfo]
|
||||
|
||||
# VM 상세 정보
|
||||
class VMDetail(VMInfo):
|
||||
rdp_port: int = 3389
|
||||
rdp_username: Optional[str] = None
|
||||
has_guest_agent: bool = False
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# VM 제어 요청
|
||||
class VMControlRequest(BaseModel):
|
||||
action: str # "start", "stop", "reboot"
|
||||
|
||||
# VM 제어 응답
|
||||
class VMControlResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
vm_id: int
|
||||
action: str
|
||||
BIN
app/services/__pycache__/auth_service.cpython-312.pyc
Normal file
BIN
app/services/__pycache__/auth_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/services/__pycache__/proxmox_service.cpython-312.pyc
Normal file
BIN
app/services/__pycache__/proxmox_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/services/__pycache__/ssh_tunnel_service.cpython-312.pyc
Normal file
BIN
app/services/__pycache__/ssh_tunnel_service.cpython-312.pyc
Normal file
Binary file not shown.
87
app/services/auth_service.py
Normal file
87
app/services/auth_service.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from app.models.user import User, UserRole
|
||||
from app.schemas.auth import UserRegister, Token
|
||||
from app.utils.security import hash_password, verify_password
|
||||
from app.utils.jwt_handler import create_access_token, create_refresh_token
|
||||
from app.utils.exceptions import AuthenticationError, ConflictError
|
||||
|
||||
class AuthService:
|
||||
"""인증 관련 비즈니스 로직"""
|
||||
|
||||
@staticmethod
|
||||
def register_user(db: Session, user_data: UserRegister) -> User:
|
||||
"""사용자 등록"""
|
||||
# 중복 체크
|
||||
if db.query(User).filter(User.username == user_data.username).first():
|
||||
raise ConflictError("이미 존재하는 사용자명입니다")
|
||||
|
||||
if db.query(User).filter(User.email == user_data.email).first():
|
||||
raise ConflictError("이미 존재하는 이메일입니다")
|
||||
|
||||
# 새 사용자 생성
|
||||
new_user = User(
|
||||
username=user_data.username,
|
||||
email=user_data.email,
|
||||
hashed_password=hash_password(user_data.password),
|
||||
full_name=user_data.full_name,
|
||||
role=UserRole.USER
|
||||
)
|
||||
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
|
||||
return new_user
|
||||
|
||||
@staticmethod
|
||||
def authenticate_user(db: Session, username: str, password: str) -> User:
|
||||
"""사용자 인증"""
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
|
||||
if not user:
|
||||
raise AuthenticationError("사용자를 찾을 수 없습니다")
|
||||
|
||||
if not user.is_active:
|
||||
raise AuthenticationError("비활성화된 계정입니다")
|
||||
|
||||
if not verify_password(password, user.hashed_password):
|
||||
raise AuthenticationError("비밀번호가 일치하지 않습니다")
|
||||
|
||||
# 마지막 로그인 시간 업데이트
|
||||
user.last_login = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def create_tokens(user: User) -> Token:
|
||||
"""JWT 토큰 생성"""
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"username": user.username,
|
||||
"role": user.role.value
|
||||
}
|
||||
|
||||
access_token = create_access_token(token_data)
|
||||
refresh_token = create_refresh_token(token_data)
|
||||
|
||||
return Token(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
token_type="bearer",
|
||||
expires_in=1800 # 30분
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_user_by_id(db: Session, user_id: int) -> Optional[User]:
|
||||
"""사용자 ID로 조회"""
|
||||
return db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_user_by_username(db: Session, username: str) -> Optional[User]:
|
||||
"""사용자명으로 조회"""
|
||||
return db.query(User).filter(User.username == username).first()
|
||||
|
||||
auth_service = AuthService()
|
||||
140
app/services/proxmox_service.py
Normal file
140
app/services/proxmox_service.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import httpx
|
||||
from typing import List, Optional, Dict
|
||||
from app.config import settings
|
||||
import json
|
||||
|
||||
class ProxmoxService:
|
||||
"""Proxmox VE API 통신 서비스"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = settings.PROXMOX_HOST
|
||||
self.api_token = settings.PROXMOX_API_TOKEN
|
||||
self.verify_ssl = settings.PROXMOX_VERIFY_SSL
|
||||
|
||||
async def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict:
|
||||
"""Proxmox API 요청"""
|
||||
url = f"{self.base_url}/api2/json{endpoint}"
|
||||
headers = {"Authorization": self.api_token}
|
||||
|
||||
async with httpx.AsyncClient(verify=self.verify_ssl, timeout=30.0) as client:
|
||||
response = await client.request(method, url, headers=headers, **kwargs)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_nodes(self) -> List[Dict]:
|
||||
"""노드 목록 조회"""
|
||||
result = await self._make_request("GET", "/nodes")
|
||||
return result.get("data", [])
|
||||
|
||||
async def get_vms(self, node: str) -> List[Dict]:
|
||||
"""특정 노드의 VM 목록 조회"""
|
||||
result = await self._make_request("GET", f"/nodes/{node}/qemu")
|
||||
return result.get("data", [])
|
||||
|
||||
async def get_all_vms(self) -> List[Dict]:
|
||||
"""모든 노드의 VM 목록 조회"""
|
||||
nodes = await self.get_nodes()
|
||||
print(f"DEBUG: 노드 목록: {nodes}")
|
||||
|
||||
all_vms = []
|
||||
|
||||
for node in nodes:
|
||||
node_name = node.get("node")
|
||||
if node_name:
|
||||
vms = await self.get_vms(node_name)
|
||||
print(f"DEBUG: {node_name} 노드의 VM 목록: {vms}")
|
||||
for vm in vms:
|
||||
vm["node"] = node_name
|
||||
all_vms.append(vm)
|
||||
|
||||
print(f"DEBUG: 전체 VM 개수: {len(all_vms)}")
|
||||
print(f"DEBUG: 전체 VM 목록: {all_vms}")
|
||||
return all_vms
|
||||
|
||||
async def get_vm_status(self, node: str, vm_id: int) -> Dict:
|
||||
"""VM 상태 조회"""
|
||||
result = await self._make_request("GET", f"/nodes/{node}/qemu/{vm_id}/status/current")
|
||||
return result.get("data", {})
|
||||
|
||||
async def get_vm_ip(self, node: str, vm_id: int) -> Optional[str]:
|
||||
"""QEMU Guest Agent를 통해 VM IP 주소 조회"""
|
||||
try:
|
||||
result = await self._make_request(
|
||||
"GET",
|
||||
f"/nodes/{node}/qemu/{vm_id}/agent/network-get-interfaces"
|
||||
)
|
||||
|
||||
interfaces = result.get("data", {}).get("result", [])
|
||||
|
||||
for iface in interfaces:
|
||||
# loopback 제외
|
||||
if "loopback" in iface.get("name", "").lower():
|
||||
continue
|
||||
|
||||
ip_addresses = iface.get("ip-addresses", [])
|
||||
for ip in ip_addresses:
|
||||
if ip.get("ip-address-type") == "ipv4":
|
||||
address = ip.get("ip-address")
|
||||
# 사설 IP만 반환
|
||||
if self._is_private_ip(address):
|
||||
return address
|
||||
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
async def start_vm(self, node: str, vm_id: int) -> bool:
|
||||
"""VM 시작"""
|
||||
try:
|
||||
await self._make_request("POST", f"/nodes/{node}/qemu/{vm_id}/status/start")
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
async def stop_vm(self, node: str, vm_id: int) -> bool:
|
||||
"""VM 종료"""
|
||||
try:
|
||||
await self._make_request("POST", f"/nodes/{node}/qemu/{vm_id}/status/stop")
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
async def reboot_vm(self, node: str, vm_id: int) -> bool:
|
||||
"""VM 재시작"""
|
||||
try:
|
||||
await self._make_request("POST", f"/nodes/{node}/qemu/{vm_id}/status/reboot")
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def _is_private_ip(self, ip: str) -> bool:
|
||||
"""사설 IP 주소 확인"""
|
||||
if not ip:
|
||||
return False
|
||||
|
||||
parts = ip.split(".")
|
||||
if len(parts) != 4:
|
||||
return False
|
||||
|
||||
try:
|
||||
first = int(parts[0])
|
||||
second = int(parts[1])
|
||||
|
||||
# 10.0.0.0/8
|
||||
if first == 10:
|
||||
return True
|
||||
|
||||
# 172.16.0.0/12
|
||||
if first == 172 and 16 <= second <= 31:
|
||||
return True
|
||||
|
||||
# 192.168.0.0/16
|
||||
if first == 192 and second == 168:
|
||||
return True
|
||||
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
# 싱글톤 인스턴스
|
||||
proxmox_service = ProxmoxService()
|
||||
220
app/services/ssh_tunnel_service.py
Normal file
220
app/services/ssh_tunnel_service.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import asyncio
|
||||
import asyncssh
|
||||
import secrets
|
||||
from typing import Dict, Optional
|
||||
from datetime import datetime
|
||||
from app.config import settings
|
||||
from app.utils.exceptions import InternalServerError
|
||||
|
||||
|
||||
class SSHTunnelManager:
|
||||
"""SSH 터널 관리"""
|
||||
|
||||
def __init__(self):
|
||||
self.active_tunnels: Dict[str, 'TunnelSession'] = {}
|
||||
self.used_ports = set()
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 포트 관리자
|
||||
# ---------------------------------------------------------
|
||||
def _get_available_port(self) -> int:
|
||||
"""사용 가능한 포트 찾기"""
|
||||
for port in range(settings.TUNNEL_PORT_MIN, settings.TUNNEL_PORT_MAX):
|
||||
if port not in self.used_ports:
|
||||
self.used_ports.add(port)
|
||||
return port
|
||||
raise InternalServerError("사용 가능한 포트가 없습니다")
|
||||
|
||||
def _release_port(self, port: int):
|
||||
"""포트 해제"""
|
||||
self.used_ports.discard(port)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 터널 생성
|
||||
# ---------------------------------------------------------
|
||||
async def create_tunnel(
|
||||
self,
|
||||
user_id: int,
|
||||
vm_id: int,
|
||||
remote_host: str,
|
||||
remote_port: int = 3389,
|
||||
vm_name: str = "",
|
||||
rdp_username: str = ""
|
||||
) -> Dict:
|
||||
"""
|
||||
SSH 터널 생성
|
||||
"""
|
||||
|
||||
session_id = secrets.token_urlsafe(32)
|
||||
local_port = self._get_available_port()
|
||||
|
||||
try:
|
||||
ssh_config = {
|
||||
"host": settings.SSH_HOST,
|
||||
"port": settings.SSH_PORT,
|
||||
"username": settings.SSH_USERNAME,
|
||||
"known_hosts": None
|
||||
}
|
||||
|
||||
if settings.SSH_KEY_PATH:
|
||||
ssh_config["client_keys"] = [settings.SSH_KEY_PATH]
|
||||
elif settings.SSH_PASSWORD:
|
||||
ssh_config["password"] = settings.SSH_PASSWORD
|
||||
else:
|
||||
raise InternalServerError("SSH 인증 정보가 설정되지 않았습니다")
|
||||
|
||||
# SSH 연결 생성
|
||||
conn = await asyncssh.connect(**ssh_config)
|
||||
|
||||
# 로컬 포트 포워딩 (외부 접속 허용)
|
||||
listener = await conn.forward_local_port(
|
||||
'0.0.0.0', # 모든 인터페이스에서 리스닝 (외부 접속 허용)
|
||||
local_port,
|
||||
remote_host,
|
||||
remote_port
|
||||
)
|
||||
|
||||
# 세션 저장
|
||||
tunnel_session = TunnelSession(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
vm_id=vm_id,
|
||||
local_port=local_port,
|
||||
remote_host=remote_host,
|
||||
remote_port=remote_port,
|
||||
connection=conn,
|
||||
listener=listener,
|
||||
created_at=datetime.utcnow(),
|
||||
vm_name=vm_name or "",
|
||||
rdp_username=rdp_username or ""
|
||||
)
|
||||
|
||||
self.active_tunnels[session_id] = tunnel_session
|
||||
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"local_port": local_port,
|
||||
"remote_host": remote_host,
|
||||
"remote_port": remote_port,
|
||||
"ssh_host": settings.SSH_HOST,
|
||||
"vm_name": vm_name or "",
|
||||
"rdp_username": rdp_username or "",
|
||||
"created_at": tunnel_session.created_at.isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self._release_port(local_port)
|
||||
raise InternalServerError(f"SSH 터널 생성 실패: {str(e)}")
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 터널 종료
|
||||
# ---------------------------------------------------------
|
||||
async def close_tunnel(self, session_id: str) -> bool:
|
||||
tunnel = self.active_tunnels.get(session_id)
|
||||
if not tunnel:
|
||||
return False
|
||||
|
||||
try:
|
||||
if tunnel.listener:
|
||||
tunnel.listener.close()
|
||||
await tunnel.listener.wait_closed()
|
||||
|
||||
if tunnel.connection:
|
||||
tunnel.connection.close()
|
||||
await tunnel.connection.wait_closed()
|
||||
|
||||
self._release_port(tunnel.local_port)
|
||||
|
||||
del self.active_tunnels[session_id]
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"터널 종료 오류: {e}")
|
||||
return False
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 터널 상태 조회
|
||||
# ---------------------------------------------------------
|
||||
async def get_tunnel_status(self, session_id: str) -> Optional[Dict]:
|
||||
tunnel = self.active_tunnels.get(session_id)
|
||||
if not tunnel:
|
||||
return None
|
||||
|
||||
is_active = tunnel.connection is not None and tunnel.listener is not None
|
||||
uptime = (datetime.utcnow() - tunnel.created_at).total_seconds()
|
||||
|
||||
# 항상 null 반환하지 않도록 빈 문자열 처리
|
||||
vm_name = tunnel.vm_name or ""
|
||||
rdp_username = tunnel.rdp_username or ""
|
||||
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"is_active": is_active,
|
||||
"uptime_seconds": int(uptime),
|
||||
"created_at": tunnel.created_at.isoformat(),
|
||||
"vm_id": tunnel.vm_id,
|
||||
"vm_name": vm_name,
|
||||
"rdp_username": rdp_username,
|
||||
"tunnel_info": {
|
||||
"session_id": session_id,
|
||||
"local_port": tunnel.local_port,
|
||||
"remote_host": tunnel.remote_host,
|
||||
"remote_port": tunnel.remote_port,
|
||||
"vm_id": tunnel.vm_id,
|
||||
"vm_name": vm_name,
|
||||
"rdp_username": rdp_username,
|
||||
"is_active": is_active
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 비활성 터널 정리
|
||||
# ---------------------------------------------------------
|
||||
async def cleanup_inactive_tunnels(self):
|
||||
to_remove = []
|
||||
|
||||
for sid, tunnel in self.active_tunnels.items():
|
||||
if tunnel.connection is None or tunnel.listener is None:
|
||||
to_remove.append(sid)
|
||||
|
||||
for sid in to_remove:
|
||||
await self.close_tunnel(sid)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 터널 세션 클래스
|
||||
# ---------------------------------------------------------
|
||||
class TunnelSession:
|
||||
"""터널 세션 정보"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session_id: str,
|
||||
user_id: int,
|
||||
vm_id: int,
|
||||
local_port: int,
|
||||
remote_host: str,
|
||||
remote_port: int,
|
||||
connection,
|
||||
listener,
|
||||
created_at: datetime,
|
||||
vm_name: str = "",
|
||||
rdp_username: str = ""
|
||||
):
|
||||
self.session_id = session_id
|
||||
self.user_id = user_id
|
||||
self.vm_id = vm_id
|
||||
self.local_port = local_port
|
||||
self.remote_host = remote_host
|
||||
self.remote_port = remote_port
|
||||
self.connection = connection
|
||||
self.listener = listener
|
||||
self.created_at = created_at
|
||||
|
||||
# 절대 null 반환 방지
|
||||
self.vm_name = vm_name or ""
|
||||
self.rdp_username = rdp_username or ""
|
||||
|
||||
|
||||
# 싱글톤 인스턴스
|
||||
ssh_tunnel_manager = SSHTunnelManager()
|
||||
78
app/services/temp_ssh_password_service.py
Normal file
78
app/services/temp_ssh_password_service.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import secrets
|
||||
import hashlib
|
||||
from typing import Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class TempSshPasswordManager:
|
||||
"""임시 SSH 비밀번호 관리"""
|
||||
|
||||
def __init__(self):
|
||||
# 메모리 기반 저장소 (프로덕션에서는 Redis 사용 권장)
|
||||
self._passwords: Dict[str, dict] = {}
|
||||
|
||||
def generate_password(self, username: str, validity_hours: int = 1) -> str:
|
||||
"""
|
||||
임시 SSH 비밀번호 생성
|
||||
|
||||
Args:
|
||||
username: 사용자명
|
||||
validity_hours: 유효 시간 (기본 1시간)
|
||||
|
||||
Returns:
|
||||
임시 비밀번호
|
||||
"""
|
||||
# 안전한 랜덤 비밀번호 생성 (32자)
|
||||
temp_password = secrets.token_urlsafe(32)
|
||||
|
||||
# 해시 저장 (실제 비밀번호는 저장하지 않음)
|
||||
password_hash = hashlib.sha256(temp_password.encode()).hexdigest()
|
||||
|
||||
# 만료 시간 계산
|
||||
expires_at = datetime.utcnow() + timedelta(hours=validity_hours)
|
||||
|
||||
# 저장
|
||||
self._passwords[username] = {
|
||||
"password_hash": password_hash,
|
||||
"expires_at": expires_at,
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
return temp_password
|
||||
|
||||
def verify_password(self, username: str, password: str) -> bool:
|
||||
"""
|
||||
비밀번호 검증
|
||||
|
||||
Args:
|
||||
username: 사용자명
|
||||
password: 검증할 비밀번호
|
||||
|
||||
Returns:
|
||||
유효 여부
|
||||
"""
|
||||
if username not in self._passwords:
|
||||
return False
|
||||
|
||||
stored = self._passwords[username]
|
||||
|
||||
# 만료 확인
|
||||
if datetime.utcnow() > stored["expires_at"]:
|
||||
del self._passwords[username]
|
||||
return False
|
||||
|
||||
# 비밀번호 확인
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
return password_hash == stored["password_hash"]
|
||||
|
||||
def cleanup_expired(self):
|
||||
"""만료된 비밀번호 정리"""
|
||||
now = datetime.utcnow()
|
||||
expired = [
|
||||
username for username, data in self._passwords.items()
|
||||
if now > data["expires_at"]
|
||||
]
|
||||
for username in expired:
|
||||
del self._passwords[username]
|
||||
|
||||
# 싱글톤 인스턴스
|
||||
temp_ssh_password_manager = TempSshPasswordManager()
|
||||
BIN
app/utils/__pycache__/exceptions.cpython-312.pyc
Normal file
BIN
app/utils/__pycache__/exceptions.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/utils/__pycache__/jwt_handler.cpython-312.pyc
Normal file
BIN
app/utils/__pycache__/jwt_handler.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/utils/__pycache__/security.cpython-312.pyc
Normal file
BIN
app/utils/__pycache__/security.cpython-312.pyc
Normal file
Binary file not shown.
31
app/utils/exceptions.py
Normal file
31
app/utils/exceptions.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
class AuthenticationError(HTTPException):
|
||||
"""인증 오류"""
|
||||
def __init__(self, detail: str = "인증에 실패했습니다"):
|
||||
super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, detail=detail)
|
||||
|
||||
class PermissionDeniedError(HTTPException):
|
||||
"""권한 부족 오류"""
|
||||
def __init__(self, detail: str = "권한이 없습니다"):
|
||||
super().__init__(status_code=status.HTTP_403_FORBIDDEN, detail=detail)
|
||||
|
||||
class NotFoundError(HTTPException):
|
||||
"""리소스를 찾을 수 없음"""
|
||||
def __init__(self, detail: str = "리소스를 찾을 수 없습니다"):
|
||||
super().__init__(status_code=status.HTTP_404_NOT_FOUND, detail=detail)
|
||||
|
||||
class BadRequestError(HTTPException):
|
||||
"""잘못된 요청"""
|
||||
def __init__(self, detail: str = "잘못된 요청입니다"):
|
||||
super().__init__(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
|
||||
|
||||
class ConflictError(HTTPException):
|
||||
"""충돌 오류 (중복 등)"""
|
||||
def __init__(self, detail: str = "이미 존재하는 리소스입니다"):
|
||||
super().__init__(status_code=status.HTTP_409_CONFLICT, detail=detail)
|
||||
|
||||
class InternalServerError(HTTPException):
|
||||
"""서버 내부 오류"""
|
||||
def __init__(self, detail: str = "서버 오류가 발생했습니다"):
|
||||
super().__init__(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=detail)
|
||||
38
app/utils/jwt_handler.py
Normal file
38
app/utils/jwt_handler.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from app.config import settings
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""JWT Access Token 생성"""
|
||||
to_encode = data.copy()
|
||||
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def create_refresh_token(data: dict) -> str:
|
||||
"""JWT Refresh Token 생성"""
|
||||
to_encode = data.copy()
|
||||
expire = datetime.utcnow() + timedelta(days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS)
|
||||
to_encode.update({"exp": expire, "type": "refresh"})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def decode_token(token: str) -> Optional[dict]:
|
||||
"""JWT 토큰 디코드"""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
|
||||
return payload
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
def verify_token(token: str) -> bool:
|
||||
"""토큰 유효성 검증"""
|
||||
payload = decode_token(token)
|
||||
return payload is not None
|
||||
12
app/utils/security.py
Normal file
12
app/utils/security.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from passlib.context import CryptContext
|
||||
|
||||
# bcrypt 컨텍스트
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""비밀번호 해시화"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""비밀번호 검증"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
api:
|
||||
build: .
|
||||
container_name: vconnect-api
|
||||
ports:
|
||||
- "8000:8000"
|
||||
env_file:
|
||||
- .env
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- vconnect-network
|
||||
|
||||
networks:
|
||||
vconnect-network:
|
||||
driver: bridge
|
||||
27
requirements.txt
Normal file
27
requirements.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
# FastAPI & Web Server
|
||||
fastapi==0.109.0
|
||||
uvicorn[standard]==0.27.0
|
||||
python-multipart==0.0.6
|
||||
|
||||
# Database
|
||||
sqlalchemy==2.0.25
|
||||
psycopg2-binary==2.9.9
|
||||
alembic==1.13.1
|
||||
|
||||
# Authentication & Security
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-dotenv==1.0.0
|
||||
pydantic-settings==2.1.0
|
||||
|
||||
# SSH & Networking
|
||||
paramiko==3.4.0
|
||||
asyncssh==2.14.2
|
||||
|
||||
# HTTP Client
|
||||
httpx==0.26.0
|
||||
aiohttp==3.9.1
|
||||
|
||||
# Utilities
|
||||
python-dateutil==2.8.2
|
||||
pytz==2023.3
|
||||
54
scripts/pam_vconnect_auth.py
Normal file
54
scripts/pam_vconnect_auth.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PAM 인증 스크립트
|
||||
임시 SSH 비밀번호 검증용
|
||||
"""
|
||||
import sys
|
||||
import requests
|
||||
import os
|
||||
|
||||
# VConnect API 서버 주소
|
||||
API_URL = os.getenv("VCONNECT_API_URL", "http://localhost:8000")
|
||||
|
||||
def verify_password(username, password):
|
||||
"""
|
||||
임시 SSH 비밀번호 검증
|
||||
|
||||
Args:
|
||||
username: 사용자명
|
||||
password: 비밀번호
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
try:
|
||||
# API 서버에 비밀번호 검증 요청
|
||||
response = requests.post(
|
||||
f"{API_URL}/api/ssh/verify",
|
||||
json={"username": username, "password": password},
|
||||
timeout=5
|
||||
)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
# PAM에서 사용자명은 환경 변수로, 비밀번호는 stdin으로 받음
|
||||
username = os.getenv("PAM_USER")
|
||||
if not username and len(sys.argv) > 1:
|
||||
username = sys.argv[1]
|
||||
|
||||
# stdin에서 비밀번호 읽기 (PAM이 제공)
|
||||
try:
|
||||
password = sys.stdin.readline().strip()
|
||||
except:
|
||||
password = ""
|
||||
|
||||
if not username or not password:
|
||||
sys.exit(1)
|
||||
|
||||
if verify_password(username, password):
|
||||
sys.exit(0) # 성공
|
||||
else:
|
||||
sys.exit(1) # 실패
|
||||
BIN
vconnect.db
Normal file
BIN
vconnect.db
Normal file
Binary file not shown.
247
venv/bin/Activate.ps1
Normal file
247
venv/bin/Activate.ps1
Normal file
@@ -0,0 +1,247 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||
70
venv/bin/activate
Normal file
70
venv/bin/activate
Normal file
@@ -0,0 +1,70 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# You cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
unset VIRTUAL_ENV_PROMPT
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||
export VIRTUAL_ENV=$(cygpath /data/vconnect-api/venv)
|
||||
else
|
||||
# use the path as-is
|
||||
export VIRTUAL_ENV=/data/vconnect-api/venv
|
||||
fi
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
PS1='(venv) '"${PS1:-}"
|
||||
export PS1
|
||||
VIRTUAL_ENV_PROMPT='(venv) '
|
||||
export VIRTUAL_ENV_PROMPT
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
27
venv/bin/activate.csh
Normal file
27
venv/bin/activate.csh
Normal file
@@ -0,0 +1,27 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
|
||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV /data/vconnect-api/venv
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
||||
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||
|
||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||
set prompt = '(venv) '"$prompt"
|
||||
setenv VIRTUAL_ENV_PROMPT '(venv) '
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
||||
69
venv/bin/activate.fish
Normal file
69
venv/bin/activate.fish
Normal file
@@ -0,0 +1,69 @@
|
||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||
# (https://fishshell.com/). You cannot run it directly.
|
||||
|
||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
set -e _OLD_VIRTUAL_PATH
|
||||
end
|
||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||
end
|
||||
|
||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
# prevents error when using nested fish instances (Issue #93858)
|
||||
if functions -q _old_fish_prompt
|
||||
functions -e fish_prompt
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
set -e VIRTUAL_ENV_PROMPT
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV /data/vconnect-api/venv
|
||||
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
||||
|
||||
# Unset PYTHONHOME if set.
|
||||
if set -q PYTHONHOME
|
||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||
set -e PYTHONHOME
|
||||
end
|
||||
|
||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
# fish uses a function instead of an env var to generate the prompt.
|
||||
|
||||
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||
functions -c fish_prompt _old_fish_prompt
|
||||
|
||||
# With the original prompt function renamed, we can override with our own.
|
||||
function fish_prompt
|
||||
# Save the return status of the last command.
|
||||
set -l old_status $status
|
||||
|
||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
|
||||
|
||||
# Restore the return status of the previous command.
|
||||
echo "exit $old_status" | .
|
||||
# Output the original/"old" prompt.
|
||||
_old_fish_prompt
|
||||
end
|
||||
|
||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||
set -gx VIRTUAL_ENV_PROMPT '(venv) '
|
||||
end
|
||||
7
venv/bin/alembic
Normal file
7
venv/bin/alembic
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from alembic.config import main
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(main())
|
||||
7
venv/bin/dotenv
Normal file
7
venv/bin/dotenv
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from dotenv.__main__ import cli
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(cli())
|
||||
7
venv/bin/email_validator
Normal file
7
venv/bin/email_validator
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from email_validator.__main__ import main
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(main())
|
||||
7
venv/bin/httpx
Normal file
7
venv/bin/httpx
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from httpx import main
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(main())
|
||||
7
venv/bin/mako-render
Normal file
7
venv/bin/mako-render
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from mako.cmd import cmdline
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(cmdline())
|
||||
8
venv/bin/pip
Normal file
8
venv/bin/pip
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
venv/bin/pip3
Normal file
8
venv/bin/pip3
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
venv/bin/pip3.12
Normal file
8
venv/bin/pip3.12
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
7
venv/bin/pyrsa-decrypt
Normal file
7
venv/bin/pyrsa-decrypt
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import decrypt
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(decrypt())
|
||||
7
venv/bin/pyrsa-encrypt
Normal file
7
venv/bin/pyrsa-encrypt
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import encrypt
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(encrypt())
|
||||
7
venv/bin/pyrsa-keygen
Normal file
7
venv/bin/pyrsa-keygen
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import keygen
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(keygen())
|
||||
7
venv/bin/pyrsa-priv2pub
Normal file
7
venv/bin/pyrsa-priv2pub
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from rsa.util import private_to_public
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(private_to_public())
|
||||
7
venv/bin/pyrsa-sign
Normal file
7
venv/bin/pyrsa-sign
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import sign
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(sign())
|
||||
7
venv/bin/pyrsa-verify
Normal file
7
venv/bin/pyrsa-verify
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import verify
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(verify())
|
||||
7
venv/bin/uvicorn
Normal file
7
venv/bin/uvicorn
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from uvicorn.main import main
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(main())
|
||||
7
venv/bin/watchfiles
Normal file
7
venv/bin/watchfiles
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from watchfiles.cli import cli
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(cli())
|
||||
7
venv/bin/websockets
Normal file
7
venv/bin/websockets
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/data/vconnect-api/venv/bin/python3
|
||||
import sys
|
||||
from websockets.cli import main
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(main())
|
||||
164
venv/include/site/python3.12/greenlet/greenlet.h
Normal file
164
venv/include/site/python3.12/greenlet/greenlet.h
Normal file
@@ -0,0 +1,164 @@
|
||||
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
|
||||
|
||||
/* Greenlet object interface */
|
||||
|
||||
#ifndef Py_GREENLETOBJECT_H
|
||||
#define Py_GREENLETOBJECT_H
|
||||
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* This is deprecated and undocumented. It does not change. */
|
||||
#define GREENLET_VERSION "1.0.0"
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
#define implementation_ptr_t void*
|
||||
#endif
|
||||
|
||||
typedef struct _greenlet {
|
||||
PyObject_HEAD
|
||||
PyObject* weakreflist;
|
||||
PyObject* dict;
|
||||
implementation_ptr_t pimpl;
|
||||
} PyGreenlet;
|
||||
|
||||
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
|
||||
|
||||
|
||||
/* C API functions */
|
||||
|
||||
/* Total number of symbols that are exported */
|
||||
#define PyGreenlet_API_pointers 12
|
||||
|
||||
#define PyGreenlet_Type_NUM 0
|
||||
#define PyExc_GreenletError_NUM 1
|
||||
#define PyExc_GreenletExit_NUM 2
|
||||
|
||||
#define PyGreenlet_New_NUM 3
|
||||
#define PyGreenlet_GetCurrent_NUM 4
|
||||
#define PyGreenlet_Throw_NUM 5
|
||||
#define PyGreenlet_Switch_NUM 6
|
||||
#define PyGreenlet_SetParent_NUM 7
|
||||
|
||||
#define PyGreenlet_MAIN_NUM 8
|
||||
#define PyGreenlet_STARTED_NUM 9
|
||||
#define PyGreenlet_ACTIVE_NUM 10
|
||||
#define PyGreenlet_GET_PARENT_NUM 11
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
/* This section is used by modules that uses the greenlet C API */
|
||||
static void** _PyGreenlet_API = NULL;
|
||||
|
||||
# define PyGreenlet_Type \
|
||||
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
|
||||
|
||||
# define PyExc_GreenletError \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
|
||||
|
||||
# define PyExc_GreenletExit \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_New(PyObject *args)
|
||||
*
|
||||
* greenlet.greenlet(run, parent=None)
|
||||
*/
|
||||
# define PyGreenlet_New \
|
||||
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
|
||||
_PyGreenlet_API[PyGreenlet_New_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetCurrent(void)
|
||||
*
|
||||
* greenlet.getcurrent()
|
||||
*/
|
||||
# define PyGreenlet_GetCurrent \
|
||||
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Throw(
|
||||
* PyGreenlet *greenlet,
|
||||
* PyObject *typ,
|
||||
* PyObject *val,
|
||||
* PyObject *tb)
|
||||
*
|
||||
* g.throw(...)
|
||||
*/
|
||||
# define PyGreenlet_Throw \
|
||||
(*(PyObject * (*)(PyGreenlet * self, \
|
||||
PyObject * typ, \
|
||||
PyObject * val, \
|
||||
PyObject * tb)) \
|
||||
_PyGreenlet_API[PyGreenlet_Throw_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
|
||||
*
|
||||
* g.switch(*args, **kwargs)
|
||||
*/
|
||||
# define PyGreenlet_Switch \
|
||||
(*(PyObject * \
|
||||
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
|
||||
_PyGreenlet_API[PyGreenlet_Switch_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
|
||||
*
|
||||
* g.parent = new_parent
|
||||
*/
|
||||
# define PyGreenlet_SetParent \
|
||||
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
|
||||
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetParent(PyObject* greenlet)
|
||||
*
|
||||
* return greenlet.parent;
|
||||
*
|
||||
* This could return NULL even if there is no exception active.
|
||||
* If it does not return NULL, you are responsible for decrementing the
|
||||
* reference count.
|
||||
*/
|
||||
# define PyGreenlet_GetParent \
|
||||
(*(PyGreenlet* (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
|
||||
|
||||
/*
|
||||
* deprecated, undocumented alias.
|
||||
*/
|
||||
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
|
||||
|
||||
# define PyGreenlet_MAIN \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
|
||||
|
||||
# define PyGreenlet_STARTED \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
|
||||
|
||||
# define PyGreenlet_ACTIVE \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
|
||||
|
||||
|
||||
|
||||
|
||||
/* Macro that imports greenlet and initializes C API */
|
||||
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
|
||||
keep the older definition to be sure older code that might have a copy of
|
||||
the header still works. */
|
||||
# define PyGreenlet_Import() \
|
||||
{ \
|
||||
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
|
||||
}
|
||||
|
||||
#endif /* GREENLET_MODULE */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* !Py_GREENLETOBJECT_H */
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright 2005-2024 SQLAlchemy authors and contributors <see AUTHORS file>.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,242 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: SQLAlchemy
|
||||
Version: 2.0.25
|
||||
Summary: Database Abstraction Library
|
||||
Home-page: https://www.sqlalchemy.org
|
||||
Author: Mike Bayer
|
||||
Author-email: mike_mp@zzzcomputing.com
|
||||
License: MIT
|
||||
Project-URL: Documentation, https://docs.sqlalchemy.org
|
||||
Project-URL: Issue Tracker, https://github.com/sqlalchemy/sqlalchemy/
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Topic :: Database :: Front-Ends
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Requires-Dist: typing-extensions >=4.6.0
|
||||
Requires-Dist: greenlet !=0.4.17 ; platform_machine == "aarch64" or (platform_machine == "ppc64le" or (platform_machine == "x86_64" or (platform_machine == "amd64" or (platform_machine == "AMD64" or (platform_machine == "win32" or platform_machine == "WIN32")))))
|
||||
Requires-Dist: importlib-metadata ; python_version < "3.8"
|
||||
Provides-Extra: aiomysql
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'aiomysql'
|
||||
Requires-Dist: aiomysql >=0.2.0 ; extra == 'aiomysql'
|
||||
Provides-Extra: aioodbc
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'aioodbc'
|
||||
Requires-Dist: aioodbc ; extra == 'aioodbc'
|
||||
Provides-Extra: aiosqlite
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'aiosqlite'
|
||||
Requires-Dist: aiosqlite ; extra == 'aiosqlite'
|
||||
Requires-Dist: typing-extensions !=3.10.0.1 ; extra == 'aiosqlite'
|
||||
Provides-Extra: asyncio
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncio'
|
||||
Provides-Extra: asyncmy
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncmy'
|
||||
Requires-Dist: asyncmy !=0.2.4,!=0.2.6,>=0.2.3 ; extra == 'asyncmy'
|
||||
Provides-Extra: mariadb_connector
|
||||
Requires-Dist: mariadb !=1.1.2,!=1.1.5,>=1.0.1 ; extra == 'mariadb_connector'
|
||||
Provides-Extra: mssql
|
||||
Requires-Dist: pyodbc ; extra == 'mssql'
|
||||
Provides-Extra: mssql_pymssql
|
||||
Requires-Dist: pymssql ; extra == 'mssql_pymssql'
|
||||
Provides-Extra: mssql_pyodbc
|
||||
Requires-Dist: pyodbc ; extra == 'mssql_pyodbc'
|
||||
Provides-Extra: mypy
|
||||
Requires-Dist: mypy >=0.910 ; extra == 'mypy'
|
||||
Provides-Extra: mysql
|
||||
Requires-Dist: mysqlclient >=1.4.0 ; extra == 'mysql'
|
||||
Provides-Extra: mysql_connector
|
||||
Requires-Dist: mysql-connector-python ; extra == 'mysql_connector'
|
||||
Provides-Extra: oracle
|
||||
Requires-Dist: cx-oracle >=8 ; extra == 'oracle'
|
||||
Provides-Extra: oracle_oracledb
|
||||
Requires-Dist: oracledb >=1.0.1 ; extra == 'oracle_oracledb'
|
||||
Provides-Extra: postgresql
|
||||
Requires-Dist: psycopg2 >=2.7 ; extra == 'postgresql'
|
||||
Provides-Extra: postgresql_asyncpg
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'postgresql_asyncpg'
|
||||
Requires-Dist: asyncpg ; extra == 'postgresql_asyncpg'
|
||||
Provides-Extra: postgresql_pg8000
|
||||
Requires-Dist: pg8000 >=1.29.1 ; extra == 'postgresql_pg8000'
|
||||
Provides-Extra: postgresql_psycopg
|
||||
Requires-Dist: psycopg >=3.0.7 ; extra == 'postgresql_psycopg'
|
||||
Provides-Extra: postgresql_psycopg2binary
|
||||
Requires-Dist: psycopg2-binary ; extra == 'postgresql_psycopg2binary'
|
||||
Provides-Extra: postgresql_psycopg2cffi
|
||||
Requires-Dist: psycopg2cffi ; extra == 'postgresql_psycopg2cffi'
|
||||
Provides-Extra: postgresql_psycopgbinary
|
||||
Requires-Dist: psycopg[binary] >=3.0.7 ; extra == 'postgresql_psycopgbinary'
|
||||
Provides-Extra: pymysql
|
||||
Requires-Dist: pymysql ; extra == 'pymysql'
|
||||
Provides-Extra: sqlcipher
|
||||
Requires-Dist: sqlcipher3-binary ; extra == 'sqlcipher'
|
||||
|
||||
SQLAlchemy
|
||||
==========
|
||||
|
||||
|PyPI| |Python| |Downloads|
|
||||
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sqlalchemy
|
||||
:target: https://pypi.org/project/sqlalchemy
|
||||
:alt: PyPI
|
||||
|
||||
.. |Python| image:: https://img.shields.io/pypi/pyversions/sqlalchemy
|
||||
:target: https://pypi.org/project/sqlalchemy
|
||||
:alt: PyPI - Python Version
|
||||
|
||||
.. |Downloads| image:: https://static.pepy.tech/badge/sqlalchemy/month
|
||||
:target: https://pepy.tech/project/sqlalchemy
|
||||
:alt: PyPI - Downloads
|
||||
|
||||
|
||||
The Python SQL Toolkit and Object Relational Mapper
|
||||
|
||||
Introduction
|
||||
-------------
|
||||
|
||||
SQLAlchemy is the Python SQL toolkit and Object Relational Mapper
|
||||
that gives application developers the full power and
|
||||
flexibility of SQL. SQLAlchemy provides a full suite
|
||||
of well known enterprise-level persistence patterns,
|
||||
designed for efficient and high-performing database
|
||||
access, adapted into a simple and Pythonic domain
|
||||
language.
|
||||
|
||||
Major SQLAlchemy features include:
|
||||
|
||||
* An industrial strength ORM, built
|
||||
from the core on the identity map, unit of work,
|
||||
and data mapper patterns. These patterns
|
||||
allow transparent persistence of objects
|
||||
using a declarative configuration system.
|
||||
Domain models
|
||||
can be constructed and manipulated naturally,
|
||||
and changes are synchronized with the
|
||||
current transaction automatically.
|
||||
* A relationally-oriented query system, exposing
|
||||
the full range of SQL's capabilities
|
||||
explicitly, including joins, subqueries,
|
||||
correlation, and most everything else,
|
||||
in terms of the object model.
|
||||
Writing queries with the ORM uses the same
|
||||
techniques of relational composition you use
|
||||
when writing SQL. While you can drop into
|
||||
literal SQL at any time, it's virtually never
|
||||
needed.
|
||||
* A comprehensive and flexible system
|
||||
of eager loading for related collections and objects.
|
||||
Collections are cached within a session,
|
||||
and can be loaded on individual access, all
|
||||
at once using joins, or by query per collection
|
||||
across the full result set.
|
||||
* A Core SQL construction system and DBAPI
|
||||
interaction layer. The SQLAlchemy Core is
|
||||
separate from the ORM and is a full database
|
||||
abstraction layer in its own right, and includes
|
||||
an extensible Python-based SQL expression
|
||||
language, schema metadata, connection pooling,
|
||||
type coercion, and custom types.
|
||||
* All primary and foreign key constraints are
|
||||
assumed to be composite and natural. Surrogate
|
||||
integer primary keys are of course still the
|
||||
norm, but SQLAlchemy never assumes or hardcodes
|
||||
to this model.
|
||||
* Database introspection and generation. Database
|
||||
schemas can be "reflected" in one step into
|
||||
Python structures representing database metadata;
|
||||
those same structures can then generate
|
||||
CREATE statements right back out - all within
|
||||
the Core, independent of the ORM.
|
||||
|
||||
SQLAlchemy's philosophy:
|
||||
|
||||
* SQL databases behave less and less like object
|
||||
collections the more size and performance start to
|
||||
matter; object collections behave less and less like
|
||||
tables and rows the more abstraction starts to matter.
|
||||
SQLAlchemy aims to accommodate both of these
|
||||
principles.
|
||||
* An ORM doesn't need to hide the "R". A relational
|
||||
database provides rich, set-based functionality
|
||||
that should be fully exposed. SQLAlchemy's
|
||||
ORM provides an open-ended set of patterns
|
||||
that allow a developer to construct a custom
|
||||
mediation layer between a domain model and
|
||||
a relational schema, turning the so-called
|
||||
"object relational impedance" issue into
|
||||
a distant memory.
|
||||
* The developer, in all cases, makes all decisions
|
||||
regarding the design, structure, and naming conventions
|
||||
of both the object model as well as the relational
|
||||
schema. SQLAlchemy only provides the means
|
||||
to automate the execution of these decisions.
|
||||
* With SQLAlchemy, there's no such thing as
|
||||
"the ORM generated a bad query" - you
|
||||
retain full control over the structure of
|
||||
queries, including how joins are organized,
|
||||
how subqueries and correlation is used, what
|
||||
columns are requested. Everything SQLAlchemy
|
||||
does is ultimately the result of a developer-initiated
|
||||
decision.
|
||||
* Don't use an ORM if the problem doesn't need one.
|
||||
SQLAlchemy consists of a Core and separate ORM
|
||||
component. The Core offers a full SQL expression
|
||||
language that allows Pythonic construction
|
||||
of SQL constructs that render directly to SQL
|
||||
strings for a target database, returning
|
||||
result sets that are essentially enhanced DBAPI
|
||||
cursors.
|
||||
* Transactions should be the norm. With SQLAlchemy's
|
||||
ORM, nothing goes to permanent storage until
|
||||
commit() is called. SQLAlchemy encourages applications
|
||||
to create a consistent means of delineating
|
||||
the start and end of a series of operations.
|
||||
* Never render a literal value in a SQL statement.
|
||||
Bound parameters are used to the greatest degree
|
||||
possible, allowing query optimizers to cache
|
||||
query plans effectively and making SQL injection
|
||||
attacks a non-issue.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Latest documentation is at:
|
||||
|
||||
https://www.sqlalchemy.org/docs/
|
||||
|
||||
Installation / Requirements
|
||||
---------------------------
|
||||
|
||||
Full documentation for installation is at
|
||||
`Installation <https://www.sqlalchemy.org/docs/intro.html#installation>`_.
|
||||
|
||||
Getting Help / Development / Bug reporting
|
||||
------------------------------------------
|
||||
|
||||
Please refer to the `SQLAlchemy Community Guide <https://www.sqlalchemy.org/support.html>`_.
|
||||
|
||||
Code of Conduct
|
||||
---------------
|
||||
|
||||
Above all, SQLAlchemy places great emphasis on polite, thoughtful, and
|
||||
constructive communication between users and developers.
|
||||
Please see our current Code of Conduct at
|
||||
`Code of Conduct <https://www.sqlalchemy.org/codeofconduct.html>`_.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
SQLAlchemy is distributed under the `MIT license
|
||||
<https://www.opensource.org/licenses/mit-license.php>`_.
|
||||
|
||||
@@ -0,0 +1,530 @@
|
||||
SQLAlchemy-2.0.25.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
SQLAlchemy-2.0.25.dist-info/LICENSE,sha256=PA9Zq4h9BB3mpOUv_j6e212VIt6Qn66abNettue-MpM,1100
|
||||
SQLAlchemy-2.0.25.dist-info/METADATA,sha256=e57J_l66lNZ5LyXmLMbAGiL02rqx7HxQu_Ci5m_5Y8U,9602
|
||||
SQLAlchemy-2.0.25.dist-info/RECORD,,
|
||||
SQLAlchemy-2.0.25.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
SQLAlchemy-2.0.25.dist-info/WHEEL,sha256=vJMp7mUkE-fMIYyE5xJ9Q2cYPnWVgHf20clVdwMSXAg,152
|
||||
SQLAlchemy-2.0.25.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11
|
||||
sqlalchemy/__init__.py,sha256=Tu8hhzZF610d9j59ruCV2IROKp-u1Y9i-Plhc6Nf50c,13033
|
||||
sqlalchemy/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/exc.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/inspection.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/log.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/schema.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__init__.py,sha256=PzXPqZqi3BzEnrs1eW0DcsR4lyknAzhhN9rWcQ97hb4,476
|
||||
sqlalchemy/connectors/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__pycache__/aioodbc.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__pycache__/asyncio.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__pycache__/pyodbc.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/aioodbc.py,sha256=GSTiNMO9h0qjPxgqaxDwWZ8HvhWMFNVR6MJQnN1oc40,5288
|
||||
sqlalchemy/connectors/asyncio.py,sha256=6s4hDYfuMjJ9KbJ4s7bF1fp5DmcgV77ozgZ5-bwZ0wc,5955
|
||||
sqlalchemy/connectors/pyodbc.py,sha256=PZC86t3poFmhgW9_tjDJH8o1Ua0OyiCdfrP7GRX5Gxc,8453
|
||||
sqlalchemy/cyextension/__init__.py,sha256=GzhhN8cjMnDTE0qerlUlpbrNmFPHQWCZ4Gk74OAxl04,244
|
||||
sqlalchemy/cyextension/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/cyextension/collections.cpython-312-x86_64-linux-gnu.so,sha256=w52mPyvsvh9eI8jjcKdy6CkeoaAQqQgyA1fsoIxbyNw,1945776
|
||||
sqlalchemy/cyextension/collections.pyx,sha256=L7DZ3DGKpgw2MT2ZZRRxCnrcyE5pU1NAFowWgAzQPEc,12571
|
||||
sqlalchemy/cyextension/immutabledict.cpython-312-x86_64-linux-gnu.so,sha256=DdLUYRGwSoHPvjZVH4IVg4juCsUDopZS8uaUFB6sgy8,811416
|
||||
sqlalchemy/cyextension/immutabledict.pxd,sha256=3x3-rXG5eRQ7bBnktZ-OJ9-6ft8zToPmTDOd92iXpB0,291
|
||||
sqlalchemy/cyextension/immutabledict.pyx,sha256=KfDTYbTfebstE8xuqAtuXsHNAK0_b5q_ymUiinUe_xs,3535
|
||||
sqlalchemy/cyextension/processors.cpython-312-x86_64-linux-gnu.so,sha256=e4g_ijRARjeUcb6O4mHKD6Si6o5syeTmmgxeYbheilY,534296
|
||||
sqlalchemy/cyextension/processors.pyx,sha256=R1rHsGLEaGeBq5VeCydjClzYlivERIJ9B-XLOJlf2MQ,1792
|
||||
sqlalchemy/cyextension/resultproxy.cpython-312-x86_64-linux-gnu.so,sha256=hyEJquVScOr748nm3DgxYuOl2-hia-4K0i6VjvWhAzw,626328
|
||||
sqlalchemy/cyextension/resultproxy.pyx,sha256=eWLdyBXiBy_CLQrF5ScfWJm7X0NeelscSXedtj1zv9Q,2725
|
||||
sqlalchemy/cyextension/util.cpython-312-x86_64-linux-gnu.so,sha256=wlYybCnldGKuyWaAF2i2-vcvCIfcq0NSleB1oeiuXlw,958968
|
||||
sqlalchemy/cyextension/util.pyx,sha256=B85orxa9LddLuQEaDoVSq1XmAXIbLKxrxpvuB8ogV_o,2530
|
||||
sqlalchemy/dialects/__init__.py,sha256=Kos9Gf5JZg1Vg6GWaCqEbD6e0r1jCwCmcnJIfcxDdcY,1770
|
||||
sqlalchemy/dialects/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/__pycache__/_typing.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/_typing.py,sha256=hyv0nKucX2gI8ispB1IsvaUgrEPn9zEcq9hS7kfstEw,888
|
||||
sqlalchemy/dialects/mssql/__init__.py,sha256=r5t8wFRNtBQoiUWh0WfIEWzXZW6f3D0uDt6NZTW_7Cc,1880
|
||||
sqlalchemy/dialects/mssql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/aioodbc.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/information_schema.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/pymssql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/pyodbc.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/aioodbc.py,sha256=UQd9ecSMIML713TDnLAviuBVJle7P7i1FtqGZZePk2Y,2022
|
||||
sqlalchemy/dialects/mssql/base.py,sha256=lkOGhA8Kg3aximjmbAOZcShXlSWLZWEe94-KdfzxkMo,133650
|
||||
sqlalchemy/dialects/mssql/information_schema.py,sha256=ZmFLZ7d4qlguBTm5pIAe3XfnCOr8qZfXPDFK5DE7in8,8083
|
||||
sqlalchemy/dialects/mssql/json.py,sha256=evUACW2O62TAPq8B7QIPagz7jfc664ql9ms68JqiYzg,4816
|
||||
sqlalchemy/dialects/mssql/provision.py,sha256=RTVbgYLFAHzEnpVQDJroU8ji_10MqBTiZfyP9_-QNT4,5362
|
||||
sqlalchemy/dialects/mssql/pymssql.py,sha256=eZRLz7HGt3SdoZUjFBmA9BS43N7AhIASw7VPBPEJuG0,4038
|
||||
sqlalchemy/dialects/mssql/pyodbc.py,sha256=GqWKptZfVMKZJ7RXQyuXL4pRJlfnOrZtRYchEeeHl3o,27057
|
||||
sqlalchemy/dialects/mysql/__init__.py,sha256=bxbi4hkysUK2OOVvr1F49akUj1cky27kKb07tgFzI9U,2153
|
||||
sqlalchemy/dialects/mysql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/aiomysql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/asyncmy.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/cymysql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/enumerated.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/expression.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mariadb.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mariadbconnector.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mysqlconnector.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mysqldb.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/pymysql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/pyodbc.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/reflection.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/reserved_words.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/aiomysql.py,sha256=tGe7R8lfaRhdRNMJHQg-AocWu5UTkstLg1C_yIKFBl8,9759
|
||||
sqlalchemy/dialects/mysql/asyncmy.py,sha256=sAz0ctNETtEc_8vl03xroLTsEWS6CLNKcqjesm4Ne6Q,9828
|
||||
sqlalchemy/dialects/mysql/base.py,sha256=Zy_ZCzuMUeeFmaOPSjmVDP5oxBEw9898QTuxppQCoq8,120698
|
||||
sqlalchemy/dialects/mysql/cymysql.py,sha256=eXT1ry0w_qRxjiO24M980c-8PZ9qSsbhqBHntjEiKB0,2300
|
||||
sqlalchemy/dialects/mysql/dml.py,sha256=HXJMAvimJsqvhj3UZO4vW_6LkF5RqaKbHvklAjor7yU,7645
|
||||
sqlalchemy/dialects/mysql/enumerated.py,sha256=ipEPPQqoXfFwcywNdcLlZCEzHBtnitHRah1Gn6nItcg,8448
|
||||
sqlalchemy/dialects/mysql/expression.py,sha256=lsmQCHKwfPezUnt27d2kR6ohk4IRFCA64KBS16kx5dc,4097
|
||||
sqlalchemy/dialects/mysql/json.py,sha256=l6MEZ0qp8FgiRrIQvOMhyEJq0q6OqiEnvDTx5Cbt9uQ,2269
|
||||
sqlalchemy/dialects/mysql/mariadb.py,sha256=kTfBLioLKk4JFFst4TY_iWqPtnvvQXFHknLfm89H2N8,853
|
||||
sqlalchemy/dialects/mysql/mariadbconnector.py,sha256=VVRwKLb6GzDmitOM4wLNvmZw6RdhnIwkLl7IZfAmUy8,8734
|
||||
sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=qiQdfLPze3QHuASAZ9iqRzD0hDW8FbKoQnfAEQCF7tM,5675
|
||||
sqlalchemy/dialects/mysql/mysqldb.py,sha256=9x_JiY4hj4tykG1ckuEGPyH4jCtsh4fgBhNukVnjUos,9658
|
||||
sqlalchemy/dialects/mysql/provision.py,sha256=4oGkClQ8jC3YLPF54sB4kCjFc8HRTwf5zl5zftAAXGo,3474
|
||||
sqlalchemy/dialects/mysql/pymysql.py,sha256=GUnSHd2M2uKjmN46Hheymtm26g7phEgwYOXrX0zLY8M,4083
|
||||
sqlalchemy/dialects/mysql/pyodbc.py,sha256=072crI4qVyPhajYvHnsfFeSrNjLFVPIjBQKo5uyz5yk,4297
|
||||
sqlalchemy/dialects/mysql/reflection.py,sha256=TsRocAsRbAisEgu5NWSND7DZg9OS9ZqwHyaxLzlEgnU,22565
|
||||
sqlalchemy/dialects/mysql/reserved_words.py,sha256=Dm7FINIAkrKLoXmdu26SpE6V8LDCGyp734nmHV2tMd0,9154
|
||||
sqlalchemy/dialects/mysql/types.py,sha256=aPzx7hqqZ21aGwByEC-yWZUl6OpMvkbxwTqdN3OUGGI,24267
|
||||
sqlalchemy/dialects/oracle/__init__.py,sha256=p4-2gw7TT0bX_MoJXTGD4i8WHctYsK9kCRbkpzykBrc,1493
|
||||
sqlalchemy/dialects/oracle/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/cx_oracle.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/dictionary.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/oracledb.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/base.py,sha256=Ng_T-Xl1rOHJO7mcnTbINnTVEQUjeLIKBl1DutaBRdY,118045
|
||||
sqlalchemy/dialects/oracle/cx_oracle.py,sha256=21xgnOZ8kCKrIfC9XlZM7R2zKZa86nsBHx4cv0Iw3x8,55290
|
||||
sqlalchemy/dialects/oracle/dictionary.py,sha256=7WMrbPkqo8ZdGjaEZyQr-5f2pajSOF1OTGb8P97z8-g,19519
|
||||
sqlalchemy/dialects/oracle/oracledb.py,sha256=vDKUuy4DExEMnXeidZL0wjuggJAqTkhXtSvxkvaEjbs,9485
|
||||
sqlalchemy/dialects/oracle/provision.py,sha256=O9ZpF4OG6Cx4mMzLRfZwhs8dZjrJETWR402n9c7726A,8304
|
||||
sqlalchemy/dialects/oracle/types.py,sha256=QK3hJvWzKnnCe3oD3rItwEEIwcoBze8qGg7VFOvVlIk,8231
|
||||
sqlalchemy/dialects/postgresql/__init__.py,sha256=rlGgUbemHlQWvsDjqWufCU-CioMPtSHRDgCd8G0v10E,3743
|
||||
sqlalchemy/dialects/postgresql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/_psycopg_common.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/array.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/asyncpg.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/ext.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/hstore.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/named_types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/operators.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/pg8000.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/pg_catalog.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg2.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg2cffi.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/ranges.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/_psycopg_common.py,sha256=7TudtgsPiSB8O5kX8W8KxcNYR8t5h_UHb86b_ChL0P8,5696
|
||||
sqlalchemy/dialects/postgresql/array.py,sha256=3EWWhFJbw2xJfie1RAqtscecCIXSGZM4qmOipLYc1T0,13691
|
||||
sqlalchemy/dialects/postgresql/asyncpg.py,sha256=KGBdzxbHnqFpWOe6usS5yxuw2KK_KBqSpp6RA7F0Ua8,40232
|
||||
sqlalchemy/dialects/postgresql/base.py,sha256=SVALrrOKYJfrlelcsUM50neHQzVAWcwA6RxQG84YM24,175627
|
||||
sqlalchemy/dialects/postgresql/dml.py,sha256=L3G3bVL41DXirham5XRBXYOM4eefhqyGCzMyn8zCdI0,11212
|
||||
sqlalchemy/dialects/postgresql/ext.py,sha256=1bZ--iNh2O9ym7l2gXZX48yP3yMO4dqb9RpYro2Mj2Q,16262
|
||||
sqlalchemy/dialects/postgresql/hstore.py,sha256=otAx-RTDfpi_tcXkMuQV0JOIXtYgevgnsikLKKOkI6U,11541
|
||||
sqlalchemy/dialects/postgresql/json.py,sha256=-ffnp85fQBOyt0Bjb7XAupmOxloUdzFZZgixUG3Wj5w,11212
|
||||
sqlalchemy/dialects/postgresql/named_types.py,sha256=i2GwHI8V83AA2Gr87yyVCNBsm9mF99as54UlyOBs7IY,17101
|
||||
sqlalchemy/dialects/postgresql/operators.py,sha256=NsAaWun_tL3d_be0fs9YL6T4LPKK6crnmFxxIJHgyeY,2808
|
||||
sqlalchemy/dialects/postgresql/pg8000.py,sha256=qnMSG3brW6XiygF-vXr1JguSSNpd-xlHJRkHHCDWs-k,18637
|
||||
sqlalchemy/dialects/postgresql/pg_catalog.py,sha256=nAKavWTE_4cqxiDKDTdo-ivkCxxRIlzD5GO9Wl1yrG4,8884
|
||||
sqlalchemy/dialects/postgresql/provision.py,sha256=yqyx-aDFO9l2YcL9f4T5HBP_Lnt5dHsMjpuXUG8mi7A,5762
|
||||
sqlalchemy/dialects/postgresql/psycopg.py,sha256=zOM1PfiGU1I-XnkF5N6pBEDvTLP0qZZg6YSWWNgd-Xw,22320
|
||||
sqlalchemy/dialects/postgresql/psycopg2.py,sha256=f6vuQ4BStwcdLRV2iVYwyqjksYSV-c_uJvCWawU6cfg,31601
|
||||
sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=M7wAYSL6Pvt-4nbfacAHGyyw4XMKJ_bQZ1tc1pBtIdg,1756
|
||||
sqlalchemy/dialects/postgresql/ranges.py,sha256=mPsXfEz3Ot0QebOqp5dt9mmy_SATwdDQ7wVe-Q5Bqqc,30252
|
||||
sqlalchemy/dialects/postgresql/types.py,sha256=yoBV_hSq6m93mtXmWPX8LoOORNqWyLC1zyckgu656BI,7323
|
||||
sqlalchemy/dialects/sqlite/__init__.py,sha256=lp9DIggNn349M-7IYhUA8et8--e8FRExWD2V_r1LJk4,1182
|
||||
sqlalchemy/dialects/sqlite/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/aiosqlite.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/pysqlcipher.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/pysqlite.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/aiosqlite.py,sha256=OMvxP2eWyqk5beF-sHhzxRmjzO4VCQp55q7NH2XPVTE,12305
|
||||
sqlalchemy/dialects/sqlite/base.py,sha256=irgfivk--Pf7nj2tIZHALGOWjeK4CDnTtvRg9vN0QjY,96794
|
||||
sqlalchemy/dialects/sqlite/dml.py,sha256=ZZ6RiyflrhtPwrgNQSYUCdUWobDnuXPN9yop0gJTm9c,8443
|
||||
sqlalchemy/dialects/sqlite/json.py,sha256=Eoplbb_4dYlfrtmQaI8Xddd2suAIHA-IdbDQYM-LIhs,2777
|
||||
sqlalchemy/dialects/sqlite/provision.py,sha256=UCpmwxf4IWlrpb2eLHGbPTpCFVbdI_KAh2mKtjiLYao,5632
|
||||
sqlalchemy/dialects/sqlite/pysqlcipher.py,sha256=OL2S_05DK9kllZj6DOz7QtEl7jI7syxjW6woS725ii4,5356
|
||||
sqlalchemy/dialects/sqlite/pysqlite.py,sha256=TAOqsHIjhbUZOF_Qk7UooiekkVZNhYJNduxlGQjokeA,27900
|
||||
sqlalchemy/dialects/type_migration_guidelines.txt,sha256=-uHNdmYFGB7bzUNT6i8M5nb4j6j9YUKAtW4lcBZqsMg,8239
|
||||
sqlalchemy/engine/__init__.py,sha256=Stb2oV6l8w65JvqEo6J4qtKoApcmOpXy3AAxQud4C1o,2818
|
||||
sqlalchemy/engine/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_processors.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_row.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_util.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/characteristics.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/create.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/cursor.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/default.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/interfaces.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/mock.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/processors.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/reflection.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/result.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/row.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/strategies.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/url.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/engine/_py_processors.py,sha256=j9i_lcYYQOYJMcsDerPxI0sVFBIlX5sqoYMdMJlgWPI,3744
|
||||
sqlalchemy/engine/_py_row.py,sha256=wSqoUFzLOJ1f89kgDb6sJm9LUrF5LMFpXPcK1vUsKcs,3787
|
||||
sqlalchemy/engine/_py_util.py,sha256=f2DI3AN1kv6EplelowesCVpwS8hSXNufRkZoQmJtSH8,2484
|
||||
sqlalchemy/engine/base.py,sha256=5n8SHmQh5Cr9DxGeRis12-_VQO4Cl7sM_FOnCBy2zD0,122207
|
||||
sqlalchemy/engine/characteristics.py,sha256=Qbvt4CPrggJ3GfxHl0hOAxopjnCQy-W_pjtwLIe-Q1g,2590
|
||||
sqlalchemy/engine/create.py,sha256=Lua52hd3e5H0a68rCVzrbwWZCnNKCef2Ew7FRchJK-c,32888
|
||||
sqlalchemy/engine/cursor.py,sha256=ErLMvqRMT8HJm-5RQeniC8-429cvXf8Sq3JSReLeVfI,74442
|
||||
sqlalchemy/engine/default.py,sha256=rZdc8JvEZCM4LtaWeJY3VsRrWMo_GDvfayK96nmGkvc,84065
|
||||
sqlalchemy/engine/events.py,sha256=c0unNFFiHzTAvkUtXoJaxzMFMDwurBkHiiUhuN8qluc,37381
|
||||
sqlalchemy/engine/interfaces.py,sha256=WE50MbuYGYhhMgF71GbfLVJAn2DDYEXiOOt6M3r-5z0,112814
|
||||
sqlalchemy/engine/mock.py,sha256=yvpxgFmRw5G4QsHeF-ZwQGHKES-HqQOucTxFtN1uzdk,4179
|
||||
sqlalchemy/engine/processors.py,sha256=XyfINKbo-2fjN-mW55YybvFyQMOil50_kVqsunahkNs,2379
|
||||
sqlalchemy/engine/reflection.py,sha256=FlT5kPpKm7Lah50GNt5XcnlJWojTL3LD_x0SoCF9kfY,75127
|
||||
sqlalchemy/engine/result.py,sha256=tGQX_zP5wlalOXXAV5BfljHd6bpSPePRXlju099TEe4,77756
|
||||
sqlalchemy/engine/row.py,sha256=S4d_WWD292B6_AnucKGG7E_6KFjEqP-BZAInrxGgicw,12080
|
||||
sqlalchemy/engine/strategies.py,sha256=DqFSWaXJPL-29Omot9O0aOcuGL8KmCGyOvnPGDkAJoE,442
|
||||
sqlalchemy/engine/url.py,sha256=dlbRISW9lMJ-Co_p1-TCXCBLMXGyrv7ROgll26FTe74,30558
|
||||
sqlalchemy/engine/util.py,sha256=hkEql1t19WHl6uzR55-F-Fs_VMCJ7p02KKQVNUDSXTk,5667
|
||||
sqlalchemy/event/__init__.py,sha256=KBrp622xojnC3FFquxa2JsMamwAbfkvzfv6Op0NKiYc,997
|
||||
sqlalchemy/event/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/api.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/attr.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/legacy.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/registry.cpython-312.pyc,,
|
||||
sqlalchemy/event/api.py,sha256=BUTAZjSlzvq4Hn2v2pihP_P1yo3lvCVDczK8lV_XJ80,8227
|
||||
sqlalchemy/event/attr.py,sha256=h8pFjHgyvLTeai6_LRQYk6ii-6M3PGgXFeZlPe-OJgo,20767
|
||||
sqlalchemy/event/base.py,sha256=LFcvzFaop51Im1IP1vz3BxFqQWXA90F2b4CMEV3JyKI,14980
|
||||
sqlalchemy/event/legacy.py,sha256=I5e9JLpRybIVlRi_ArEwymwbfx8vNns52v9QWaqVtA4,8211
|
||||
sqlalchemy/event/registry.py,sha256=LKbNsF5quf0DAGCw8jaGLmSf7p9ejWjT2y_Oz7FoDNo,10833
|
||||
sqlalchemy/events.py,sha256=k-ZD38aSPD29LYhED7CBqttp5MDVVx_YSaWC2-cu9ec,525
|
||||
sqlalchemy/exc.py,sha256=GRMcfOg64pRXX7nGsDt5iXfwqUdiMILstYse87vzmLI,24000
|
||||
sqlalchemy/ext/__init__.py,sha256=S1fGKAbycnQDV01gs-JWGaFQ9GCD4QHwKcU2wnugg_o,322
|
||||
sqlalchemy/ext/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/associationproxy.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/automap.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/baked.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/compiler.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/horizontal_shard.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/hybrid.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/indexable.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/instrumentation.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/mutable.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/orderinglist.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/serializer.cpython-312.pyc,,
|
||||
sqlalchemy/ext/associationproxy.py,sha256=BPYsvBlh8KVxKPLhHc31_JTatk7bhrSJTqO50b3OLcA,65960
|
||||
sqlalchemy/ext/asyncio/__init__.py,sha256=1OqSxEyIUn7RWLGyO12F-jAUIvk1I6DXlVy80-Gvkds,1317
|
||||
sqlalchemy/ext/asyncio/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/engine.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/exc.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/result.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/scoping.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/session.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/base.py,sha256=HVpevdn2vcCrhWyeSDdP0JFm2SdEIitmBlXv82Yywbo,8937
|
||||
sqlalchemy/ext/asyncio/engine.py,sha256=vy8_HiMFP4HWXvqLSES0AMQIWWNUmrIxPIeVHa6QbEw,48058
|
||||
sqlalchemy/ext/asyncio/exc.py,sha256=8sII7VMXzs2TrhizhFQMzSfcroRtiesq8o3UwLfXSgQ,639
|
||||
sqlalchemy/ext/asyncio/result.py,sha256=pVBeJym7zjT4eMcduU3X2_g5qtnaKuy2kIJWVJGTv_o,30554
|
||||
sqlalchemy/ext/asyncio/scoping.py,sha256=xLdjNJ1VnlTmi5YAsZKQo3XfBzKpviKpLG_HeSSbv2g,52685
|
||||
sqlalchemy/ext/asyncio/session.py,sha256=sc9SKwqEgPiTIjlIyVa679F-Q2CWp6_8ucJOis_hYaQ,62998
|
||||
sqlalchemy/ext/automap.py,sha256=MTvMs97xALDugdgyY3JqiVeusrcKruN0BR7iSFhNZOg,61431
|
||||
sqlalchemy/ext/baked.py,sha256=H6T1il7GY84BhzPFj49UECSpZh_eBuiHomA-QIsYOYQ,17807
|
||||
sqlalchemy/ext/compiler.py,sha256=ONPoxoKD2yUS9R2-oOhmPsA7efm-Bs0BXo7HE1dGlsU,20391
|
||||
sqlalchemy/ext/declarative/__init__.py,sha256=20psLdFQbbOWfpdXHZ0CTY6I1k4UqXvKemNVu1LvPOI,1818
|
||||
sqlalchemy/ext/declarative/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/declarative/__pycache__/extensions.cpython-312.pyc,,
|
||||
sqlalchemy/ext/declarative/extensions.py,sha256=uCjN1GisQt54AjqYnKYzJdUjnGd2pZBW47WWdPlS7FE,19547
|
||||
sqlalchemy/ext/horizontal_shard.py,sha256=ITc2MU4pVc6t_HLR-T6tTMvYbwrl8cM9Bi02jI4e7FM,16766
|
||||
sqlalchemy/ext/hybrid.py,sha256=iU2GCE-PDHWa6hp3-nv9fNjuk46hzmRPJKxHEs7smxY,52514
|
||||
sqlalchemy/ext/indexable.py,sha256=UkTelbydKCdKelzbv3HWFFavoET9WocKaGRPGEOVfN8,11032
|
||||
sqlalchemy/ext/instrumentation.py,sha256=7908PLZGlD9tiq0nWS0A7iNNax0iGNsIchhJ_OoxOCg,15723
|
||||
sqlalchemy/ext/mutable.py,sha256=IX_H7vCBG834gFtCQzopuwgpmEDspBQBdb_nK-Dfrfo,37427
|
||||
sqlalchemy/ext/mypy/__init__.py,sha256=0WebDIZmqBD0OTq5JLtd_PmfF9JGxe4d4Qv3Ml3PKUg,241
|
||||
sqlalchemy/ext/mypy/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/apply.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/decl_class.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/infer.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/names.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/plugin.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/apply.py,sha256=KZP0RAsQd65hyoS9bOBZ0UtgA1sZSd1wH9S0jyc_v08,10508
|
||||
sqlalchemy/ext/mypy/decl_class.py,sha256=gQOIMZOpKKdFW1TQjnEVM2RxGJbZzz5dFeMWRaIoFHc,17382
|
||||
sqlalchemy/ext/mypy/infer.py,sha256=KVnmLFEVS33Al8pUKI7MJbJQu3KeveBUMl78EluBORw,19369
|
||||
sqlalchemy/ext/mypy/names.py,sha256=IQ16GLZFqKxfYxIZxkbTurBqOUYbUV-64V_DSRns1tc,10630
|
||||
sqlalchemy/ext/mypy/plugin.py,sha256=74ML8LI9xar0V86oCxnPFv5FQGEEfUzK64vOay4BKFs,9750
|
||||
sqlalchemy/ext/mypy/util.py,sha256=DLvKhM38mZk_1vvxbpenq-krTwdqdrZbg-o072TodCs,9408
|
||||
sqlalchemy/ext/orderinglist.py,sha256=TGYbsGH72wEZcFNQDYDsZg9OSPuzf__P8YX8_2HtYUo,14384
|
||||
sqlalchemy/ext/serializer.py,sha256=YemanWdeMVUDweHCnQc-iMO6mVVXNo2qQ5NK0Eb2_Es,6178
|
||||
sqlalchemy/future/__init__.py,sha256=q2mw-gxk_xoxJLEvRoyMha3vO1xSRHrslcExOHZwmPA,512
|
||||
sqlalchemy/future/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/future/__pycache__/engine.cpython-312.pyc,,
|
||||
sqlalchemy/future/engine.py,sha256=AgIw6vMsef8W6tynOTkxsjd6o_OQDwGjLdbpoMD8ue8,495
|
||||
sqlalchemy/inspection.py,sha256=fLQyZnIS7irH7dRX3AaUHCkCArjIxMboaJj0rjM3cnA,5137
|
||||
sqlalchemy/log.py,sha256=KPxnFg6D2kj2_-3k6njiwxqbqyj51lid-O8ZWZN3HIg,8623
|
||||
sqlalchemy/orm/__init__.py,sha256=ZYys5nL3RFUDCMOLFDBrRI52F6er3S1U1OY9TeORuKs,8463
|
||||
sqlalchemy/orm/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/_orm_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/_typing.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/attributes.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/bulk_persistence.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/clsregistry.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/collections.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/context.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/decl_api.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/decl_base.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/dependency.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/descriptor_props.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/dynamic.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/evaluator.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/exc.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/identity.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/instrumentation.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/interfaces.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/loading.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/mapped_collection.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/mapper.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/path_registry.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/persistence.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/properties.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/query.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/relationships.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/scoping.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/session.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/state.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/state_changes.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/strategies.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/strategy_options.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/sync.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/unitofwork.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/writeonly.cpython-312.pyc,,
|
||||
sqlalchemy/orm/_orm_constructors.py,sha256=hhvHmDbY2cbMedLbLRYVAh14XO1U1Co513qzIke0THY,99370
|
||||
sqlalchemy/orm/_typing.py,sha256=RhN8pyEBjpYzGaxezyZCPRNV82U--BMRTIDdIiogsyo,5024
|
||||
sqlalchemy/orm/attributes.py,sha256=2R9p508nfGDwj_qr4Hxbv_g2QDDQSp0-n_GCHcjqb2U,92578
|
||||
sqlalchemy/orm/base.py,sha256=4-AzRGLdOTCOpgg8larSWb6s-YOqbMLrgQPhw__51ys,27700
|
||||
sqlalchemy/orm/bulk_persistence.py,sha256=vjx8DSvbeyTzDiuwaBonZ8TDc6kfUtBIEaXJZEuY6ac,69878
|
||||
sqlalchemy/orm/clsregistry.py,sha256=W9Su2JwNCvJuJiAyOT49ALUj59G36z4ugJimkKId530,17962
|
||||
sqlalchemy/orm/collections.py,sha256=t184F_YbXxZdwLOyv85l4M32gbJ-EQtbdMewnsmfdHg,52159
|
||||
sqlalchemy/orm/context.py,sha256=esaaJ7pYE4-SzXqeEE-Pv7BZLtnghwMY2dasW9N1vnY,111893
|
||||
sqlalchemy/orm/decl_api.py,sha256=YrN1zaL7yFAXFtzJYkzC6DUMaqfEenCdQnBGSK1Xn-E,63882
|
||||
sqlalchemy/orm/decl_base.py,sha256=WfKe0wyxrYr1-mmeiQgfIhRD_PHlFK-xDNsvYJ3R0T0,81621
|
||||
sqlalchemy/orm/dependency.py,sha256=lrTu8yfqLbz7U0iOHULR_Yk4C0z-2VDtFpuH7TaeynA,47583
|
||||
sqlalchemy/orm/descriptor_props.py,sha256=RennfXQ7bdnANgNF2SlB_-W_GlaZ26SsdhI4Yb_orYE,37176
|
||||
sqlalchemy/orm/dynamic.py,sha256=toSmHi9AF9nnbZmvLSMOSGR0NaG0YpszlLQx8KnxMbk,9798
|
||||
sqlalchemy/orm/evaluator.py,sha256=q292K5vdpP69G7Z9y1RqI5GFAk2diUPwnsXE8De_Wgw,11925
|
||||
sqlalchemy/orm/events.py,sha256=USrIP-2JlcIbmssvCkea1veL3eIIWC7WH7KDmTzqa-Q,127601
|
||||
sqlalchemy/orm/exc.py,sha256=w7MZkJMGGlu5J6jOFSmi9XXzc02ctnTv34jrEWpI-eM,7356
|
||||
sqlalchemy/orm/identity.py,sha256=jHdCxCpCyda_8mFOfGmN_Pr0XZdKiU-2hFZshlNxbHs,9249
|
||||
sqlalchemy/orm/instrumentation.py,sha256=wHdGTYpzND7nhgbpmiryLOXuWLIzCVii6jpfVWAi2RQ,24337
|
||||
sqlalchemy/orm/interfaces.py,sha256=0cK5udFIGusQ3cW697zXEElIPVafxW5UD5KL_6oNi8w,48404
|
||||
sqlalchemy/orm/loading.py,sha256=GKvLzmFklYgS89enTt_b2fvyDnu9rUAlYLL-Oa9eAeM,57417
|
||||
sqlalchemy/orm/mapped_collection.py,sha256=IsSSxYWuR396Qep4MYCe2VDmFr5sU_3AuDrhXDf_mVA,19704
|
||||
sqlalchemy/orm/mapper.py,sha256=j5r6ezZKW_WOcp_SIz0neMHMXiAMriVMdXbgvIhusfg,170969
|
||||
sqlalchemy/orm/path_registry.py,sha256=uZULekFBpy292nYuE0ON6vGcgM0Szs6a0iN7Wyya0_A,25938
|
||||
sqlalchemy/orm/persistence.py,sha256=2nQZpi9Mi-uKlJ-cwLodOMu-9gs4ZpYcUseIk4T210M,60989
|
||||
sqlalchemy/orm/properties.py,sha256=1gaf8QaGunBN2K2nEvMcucD4U1cOntJgsqJafLtHi7w,29095
|
||||
sqlalchemy/orm/query.py,sha256=QwUV1vm-6gyriYiZ4GIoNV3rdwdaCFhfdChTvVq00Oo,117714
|
||||
sqlalchemy/orm/relationships.py,sha256=NRQBABfdAWWqtRzhyE8Xq_uDeFPLxruSBNK646yv-vo,127619
|
||||
sqlalchemy/orm/scoping.py,sha256=Aeu34zEhcxcbS8NCzfgHzDBhlSXnlay5Ln8SPczdh9k,78821
|
||||
sqlalchemy/orm/session.py,sha256=bJTTXE7yB4qD6JSe0lvJv8QASf2OoUOoHQplt1ODHJk,193265
|
||||
sqlalchemy/orm/state.py,sha256=M60-bI0R0dzGHc-fBNGXX1V8osNTSC_WwtmlhcErSBM,37536
|
||||
sqlalchemy/orm/state_changes.py,sha256=qKYg7NxwrDkuUY3EPygAztym6oAVUFcP2wXn7QD3Mz4,6815
|
||||
sqlalchemy/orm/strategies.py,sha256=u6d7F5cAi2TC4NNYzFecoMYEUownpQcght5hIXVPJ7M,114052
|
||||
sqlalchemy/orm/strategy_options.py,sha256=267SGNfWJGlWjrdBffX8cQYrN7Ilk3Xk4kL4J150P7U,84161
|
||||
sqlalchemy/orm/sync.py,sha256=g7iZfSge1HgxMk9SKRgUgtHEbpbZ1kP_CBqOIdTOXqc,5779
|
||||
sqlalchemy/orm/unitofwork.py,sha256=fiVaqcymbDDHRa1NjS90N9Z466nd5pkJOEi1dHO6QLY,27033
|
||||
sqlalchemy/orm/util.py,sha256=j6BLkPtzZs88tINO21h-Dv3w2N1mlRZNabB_Q27U9ac,80340
|
||||
sqlalchemy/orm/writeonly.py,sha256=xh-KN8CiykLNQ_L9HE8QM1A822CJfiYopG9snSbWsz0,22329
|
||||
sqlalchemy/pool/__init__.py,sha256=qiDdq4r4FFAoDrK6ncugF_i6usi_X1LeJt-CuBHey0s,1804
|
||||
sqlalchemy/pool/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/pool/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/pool/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/pool/__pycache__/impl.cpython-312.pyc,,
|
||||
sqlalchemy/pool/base.py,sha256=2c614izCvoBw0xxu0LOhtziVB_G_MGns0jZu6ts9Bn8,52243
|
||||
sqlalchemy/pool/events.py,sha256=12gjivfkZkROTFYoIUS0kkvt3Ftj5ogBGavP9oSFPl4,13137
|
||||
sqlalchemy/pool/impl.py,sha256=2gdX23oZPLEqf3phI8yJod4ElB1BwFWkZCAtEXjXqbM,17718
|
||||
sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
sqlalchemy/schema.py,sha256=dKiWmgHYjcKQ4TiiD6vD0UMmIsD8u0Fsor1M9AAeGUs,3194
|
||||
sqlalchemy/sql/__init__.py,sha256=UNa9EUiYWoPayf-FzNcwVgQvpsBdInPZfpJesAStN9o,5820
|
||||
sqlalchemy/sql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_dml_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_elements_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_orm_types.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_py_util.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_selectable_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_typing.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/annotation.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/cache_key.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/coercions.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/compiler.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/crud.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/ddl.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/default_comparator.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/elements.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/expression.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/functions.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/lambdas.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/naming.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/operators.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/roles.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/schema.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/selectable.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/sqltypes.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/traversals.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/type_api.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/visitors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/_dml_constructors.py,sha256=YdBJex0MCVACv4q2nl_ii3uhxzwU6aDB8zAsratX5UQ,3867
|
||||
sqlalchemy/sql/_elements_constructors.py,sha256=oCro-h7QqOIbq4abduZP8EIw88HbHhPWPdyN8fzMuas,62558
|
||||
sqlalchemy/sql/_orm_types.py,sha256=T-vjcry4C1y0GToFKVxQCnmly_-Zsq4IO4SHN6bvUF4,625
|
||||
sqlalchemy/sql/_py_util.py,sha256=hiM9ePbRSGs60bAMxPFuJCIC_p9SQ1VzqXGiPchiYwE,2173
|
||||
sqlalchemy/sql/_selectable_constructors.py,sha256=kHm45Q6t2QCHIj2CS0SEZvD0MwKmK59lmg55NQMMKK8,18812
|
||||
sqlalchemy/sql/_typing.py,sha256=XDr2i-6GxPLBvtJC3ybzffQubUv_h3I2xRDUBlQLg30,12613
|
||||
sqlalchemy/sql/annotation.py,sha256=fEeyIJYId-xVHxp4VuHUzOnosa3iKLAHExnb8swq8v0,18271
|
||||
sqlalchemy/sql/base.py,sha256=vnkJFb9yJwTFX9Fd0CsISiP9m3PBfNQiBfMCmfUt69k,73928
|
||||
sqlalchemy/sql/cache_key.py,sha256=mTIa7UfTiUpeXDcVqn5vDBf-soj81fXOIHLPNjoQzhg,33124
|
||||
sqlalchemy/sql/coercions.py,sha256=BKj_pkSS4zBmc3tiS8wWmNhGWeyr66sK4QHJudqy1Lg,40489
|
||||
sqlalchemy/sql/compiler.py,sha256=WgnzXhjGvHxEC4VhIXgoeCoyKDFjRGfIwv0rH4Ug6vw,269842
|
||||
sqlalchemy/sql/crud.py,sha256=Xp3rX7N-YuecL14StUEc959a6oj61EG_fiyoiVYGAqY,56457
|
||||
sqlalchemy/sql/ddl.py,sha256=GJfH800KdwMhBbhp1f6FFyDVx-RjtG2WRGa8UTVaZI4,45542
|
||||
sqlalchemy/sql/default_comparator.py,sha256=1OiYbEojh6Vq8gy_jqY3b19dYZ0DvjFbyV5QeFiAs_o,16646
|
||||
sqlalchemy/sql/dml.py,sha256=BXUkqWPhnELKrhRKhfWZH4YZXKCCDCoHemVnfaew8Us,65728
|
||||
sqlalchemy/sql/elements.py,sha256=sv1D2nLZO0rmnmcrPuQcMAp2YCkTWv_r4GBG_xDlSbY,172784
|
||||
sqlalchemy/sql/events.py,sha256=iC_Q1Htm1Aobt5tOYxWfHHqNpoytrULORmUKcusH_-E,18290
|
||||
sqlalchemy/sql/expression.py,sha256=VMX-dLpsZYnVRJpYNDozDUgaj7iQ0HuewUKVefD57PE,7586
|
||||
sqlalchemy/sql/functions.py,sha256=e_29NAfsMBltrVVlgVUDt2CizHXrtS8dVBzvqnXgMJg,64248
|
||||
sqlalchemy/sql/lambdas.py,sha256=bG3D175kcQ3-9OWMLABSa6-diJyOfMf0ajr2w4mFdfM,49281
|
||||
sqlalchemy/sql/naming.py,sha256=ZHs1qSV3ou8TYmZ92uvU3sfdklUQlIz4uhe330n05SU,6858
|
||||
sqlalchemy/sql/operators.py,sha256=eCADkzisFq3PPWUSN28HYe8sgpxFQe6kJUATPn-XsuM,76193
|
||||
sqlalchemy/sql/roles.py,sha256=VwwJacCiopxIHABAeNgLApDxo-xHPhJl7UfdLebMJGw,7686
|
||||
sqlalchemy/sql/schema.py,sha256=XO0fxfaBbnyJjlunZln_Xi8ecPM_l7WZowgyi-btAek,228211
|
||||
sqlalchemy/sql/selectable.py,sha256=_Bu-nSxEC3kSUosAsDsZZn7S7BC_HtbFainREIQK6D8,233041
|
||||
sqlalchemy/sql/sqltypes.py,sha256=Bv_xlg27fsYN0xPQ81LaQ-snVm2JqsX_Rz1IpkUBqpw,126509
|
||||
sqlalchemy/sql/traversals.py,sha256=Tvt6DFCGIqsQHSCfKzk2kAbi_bnEX3jh_8ZRdtOFoYE,33521
|
||||
sqlalchemy/sql/type_api.py,sha256=PhMBSDza_dT5RXSPNkpTJUqGj2ojI_2vSkOSWl5OuRQ,83883
|
||||
sqlalchemy/sql/util.py,sha256=LVLjLqpZuJ_DT6oUeE7WJbuCkIMEA8ZUEbO204Gp06U,48187
|
||||
sqlalchemy/sql/visitors.py,sha256=St-h4A5ZMVMGu9hFhbGZJUxa9KaEg7e2KaCMihVTU64,36427
|
||||
sqlalchemy/testing/__init__.py,sha256=VsrEHrORpAF5n7Vfl43YQgABh6EP1xBx_gHxs7pSXeE,3126
|
||||
sqlalchemy/testing/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/assertions.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/assertsql.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/asyncio.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/config.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/engines.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/entities.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/exclusions.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/pickleable.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/profiling.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/requirements.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/schema.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/warnings.cpython-312.pyc,,
|
||||
sqlalchemy/testing/assertions.py,sha256=gL0rA7CCZJbcVgvWOPV91tTZTRwQc1_Ta0-ykBn83Ew,31439
|
||||
sqlalchemy/testing/assertsql.py,sha256=xwo0ZuCN69Y0ElCZys3lnmPxHdmJ34E2Cns4V3g8MA0,16817
|
||||
sqlalchemy/testing/asyncio.py,sha256=fkdRz-E37d5OrQKw5hdjmglOTJyXGnJzaJpvNXOBLxg,3728
|
||||
sqlalchemy/testing/config.py,sha256=9HWOgvPLSRJIjRWa0wauo3klYafV9oEhp8qhjptvlVw,12030
|
||||
sqlalchemy/testing/engines.py,sha256=iB3dLHhSBLPbTB5lSYnnppnfB66IP2DmfW56mhH4sjI,13355
|
||||
sqlalchemy/testing/entities.py,sha256=IphFegPKbff3Un47jY6bi7_MQXy6qkx_50jX2tHZJR4,3354
|
||||
sqlalchemy/testing/exclusions.py,sha256=vZlqF8Jy_PpXc9e-yPAI8-UHFVM-UiRfMwI-WrWS-nU,12444
|
||||
sqlalchemy/testing/fixtures/__init__.py,sha256=dMClrIoxqlYIFpk2ia4RZpkbfxsS_3EBigr9QsPJ66g,1198
|
||||
sqlalchemy/testing/fixtures/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/mypy.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/orm.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/sql.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/base.py,sha256=9r_J2ksiTzClpUxW0TczICHrWR7Ny8PV8IsBz6TsGFI,12256
|
||||
sqlalchemy/testing/fixtures/mypy.py,sha256=nrfgQnzIZoRFJ47F-7IZpouvAq6mSQHb8A-TbiDxv5I,11845
|
||||
sqlalchemy/testing/fixtures/orm.py,sha256=8EFbnaBbXX_Bf4FcCzBUaAHgyVpsLGBHX16SGLqE3Fg,6095
|
||||
sqlalchemy/testing/fixtures/sql.py,sha256=MFOuYBUyPIpHJzjRCHL9vU-IT4bD6LXGGMvsp0v1FY8,15704
|
||||
sqlalchemy/testing/pickleable.py,sha256=U9mIqk-zaxq9Xfy7HErP7UrKgTov-A3QFnhZh-NiOjI,2833
|
||||
sqlalchemy/testing/plugin/__init__.py,sha256=79F--BIY_NTBzVRIlJGgAY5LNJJ3cD19XvrAo4X0W9A,247
|
||||
sqlalchemy/testing/plugin/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/bootstrap.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/plugin_base.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/pytestplugin.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/bootstrap.py,sha256=oYScMbEW4pCnWlPEAq1insFruCXFQeEVBwo__i4McpU,1685
|
||||
sqlalchemy/testing/plugin/plugin_base.py,sha256=IR2tLVvW7dbAqagFYwUjsc2X1oVIRVp1GM_b6tZTQkw,21581
|
||||
sqlalchemy/testing/plugin/pytestplugin.py,sha256=xasjXXEMsT4RtKR7WzmEqzYXPxz83KSRaM2CwJQEQK8,27546
|
||||
sqlalchemy/testing/profiling.py,sha256=PbuPhRFbauFilUONeY3tV_Y_5lBkD7iCa8VVyH2Sk9Y,10148
|
||||
sqlalchemy/testing/provision.py,sha256=zXsw2D2Xpmw_chmYLsE1GXQqKQ-so3V8xU_joTcKan0,14619
|
||||
sqlalchemy/testing/requirements.py,sha256=N9pSj7z2wVMkBif-DQfPVa_cl9k6p9g_J5FY1OsWtrY,51817
|
||||
sqlalchemy/testing/schema.py,sha256=lr4GkGrGwagaHMuSGzWdzkMaj3HnS7dgfLLWfxt__-U,6513
|
||||
sqlalchemy/testing/suite/__init__.py,sha256=Y5DRNG0Yl1u3ypt9zVF0Z9suPZeuO_UQGLl-wRgvTjU,722
|
||||
sqlalchemy/testing/suite/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_cte.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_ddl.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_deprecations.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_dialect.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_insert.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_reflection.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_results.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_rowcount.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_select.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_sequence.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_types.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_unicode_ddl.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_update_delete.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/test_cte.py,sha256=6zBC3W2OwX1Xs-HedzchcKN2S7EaLNkgkvV_JSZ_Pq0,6451
|
||||
sqlalchemy/testing/suite/test_ddl.py,sha256=1Npkf0C_4UNxphthAGjG078n0vPEgnSIHpDu5MfokxQ,12031
|
||||
sqlalchemy/testing/suite/test_deprecations.py,sha256=BcJxZTcjYqeOAENVElCg3hVvU6fkGEW3KGBMfnW8bng,5337
|
||||
sqlalchemy/testing/suite/test_dialect.py,sha256=EH4ZQWbnGdtjmx5amZtTyhYmrkXJCvW1SQoLahoE7uk,22923
|
||||
sqlalchemy/testing/suite/test_insert.py,sha256=8ASo87s2pvaFkYZbmx5zYDGsRloyGpI9Zo5ut4ttM9g,18557
|
||||
sqlalchemy/testing/suite/test_reflection.py,sha256=_44jrB9iQ0PbQ3Aj2nz45eJIkDmCGJ5I8r4Q2-Km9Ww,106458
|
||||
sqlalchemy/testing/suite/test_results.py,sha256=NQ23m8FDVd0ub751jN4PswGoAhk5nrqvjHvpYULZXnc,15937
|
||||
sqlalchemy/testing/suite/test_rowcount.py,sha256=Ozu9NmGrsMWROGNdtE-KKOaa_WMHnvSy-qcnAYpG20Y,7903
|
||||
sqlalchemy/testing/suite/test_select.py,sha256=FvMFYQW9IJpDWGYZiJk46is6YrtmdSghBdTjZCG8T0Y,58574
|
||||
sqlalchemy/testing/suite/test_sequence.py,sha256=66bCoy4xo99GBSaX6Hxb88foANAykLGRz1YEKbvpfuA,9923
|
||||
sqlalchemy/testing/suite/test_types.py,sha256=rFmTOg6XuMch9L2-XthfLJRCTTwpZbMfrNss2g09gmc,65677
|
||||
sqlalchemy/testing/suite/test_unicode_ddl.py,sha256=c3_eIxLyORuSOhNDP0jWKxPyUf3SwMFpdalxtquwqlM,6141
|
||||
sqlalchemy/testing/suite/test_update_delete.py,sha256=3aaM9LtV_qC8NzMcOn2BZHxnQ5eWJnFKOx6iQpGArSw,3914
|
||||
sqlalchemy/testing/util.py,sha256=BFiSp3CEX95Dr-vv4l_7ZRu5vjZi9hjjnp-JKNfuS5E,14080
|
||||
sqlalchemy/testing/warnings.py,sha256=fJ-QJUY2zY2PPxZJKv9medW-BKKbCNbA4Ns_V3YwFXM,1546
|
||||
sqlalchemy/types.py,sha256=cQFM-hFRmaf1GErun1qqgEs6QxufvzMuwKqj9tuMPpE,3168
|
||||
sqlalchemy/util/__init__.py,sha256=B3bedg-LSQEscwqgmYYU-VENUX8_zAE3q9vb7tkfJNY,8277
|
||||
sqlalchemy/util/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_collections.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_concurrency_py3k.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_has_cy.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_py_collections.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/compat.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/concurrency.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/deprecations.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/langhelpers.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/preloaded.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/queue.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/tool_support.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/topological.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/typing.cpython-312.pyc,,
|
||||
sqlalchemy/util/_collections.py,sha256=py6nwBAgPC_pmbghrobaCVZZxmiVuLgqrbjdgqP_3jU,20111
|
||||
sqlalchemy/util/_concurrency_py3k.py,sha256=3jieKUZN8sS-RS6L0a2BUCUm_pqQMldbuFCxZ7xr1-Q,8602
|
||||
sqlalchemy/util/_has_cy.py,sha256=wCQmeSjT3jaH_oxfCEtGk-1g0gbSpt5MCK5UcWdMWqk,1247
|
||||
sqlalchemy/util/_py_collections.py,sha256=AZEigbk8jfiKjH3Rqy6BBmRwe8csZuRZ5lz2DYS-Cls,16738
|
||||
sqlalchemy/util/compat.py,sha256=R6bpBydldtbr6h7oJePihQxFb7jKiI-YDsK465MSOzk,8714
|
||||
sqlalchemy/util/concurrency.py,sha256=mhwHm0utriD14DRqxTBWgIW7QuwdSEiLgLiJdUjiR3w,2427
|
||||
sqlalchemy/util/deprecations.py,sha256=YBwvvYhSB8LhasIZRKvg_-WNoVhPUcaYI1ZrnjDn868,11971
|
||||
sqlalchemy/util/langhelpers.py,sha256=HEdM_PcZxoSXH9qlcvf5Vp1k4xKU2XJBwn0Aq9RVmeU,64980
|
||||
sqlalchemy/util/preloaded.py,sha256=az7NmLJLsqs0mtM9uBkIu10-841RYDq8wOyqJ7xXvqE,5904
|
||||
sqlalchemy/util/queue.py,sha256=_5qHVIvWluQQt0jiDHnxuT2FhUxnxH2cKDxCmwmTFe0,10205
|
||||
sqlalchemy/util/tool_support.py,sha256=9braZyidaiNrZVsWtGmkSmus50-byhuYrlAqvhjcmnA,6135
|
||||
sqlalchemy/util/topological.py,sha256=N3M3Le7KzGHCmqPGg0ZBqixTDGwmFLhOZvBtc4rHL_g,3458
|
||||
sqlalchemy/util/typing.py,sha256=-YKRlPyicpM94qhFAUj9eYnv2hnKHkhv0Gxq7jNPAvE,16255
|
||||
@@ -0,0 +1,6 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.42.0)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp312-cp312-manylinux_2_17_x86_64
|
||||
Tag: cp312-cp312-manylinux2014_x86_64
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
sqlalchemy
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
33
venv/lib/python3.12/site-packages/_yaml/__init__.py
Normal file
33
venv/lib/python3.12/site-packages/_yaml/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# This is a stub package designed to roughly emulate the _yaml
|
||||
# extension module, which previously existed as a standalone module
|
||||
# and has been moved into the `yaml` package namespace.
|
||||
# It does not perfectly mimic its old counterpart, but should get
|
||||
# close enough for anyone who's relying on it even when they shouldn't.
|
||||
import yaml
|
||||
|
||||
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
|
||||
# to tread carefully when poking at it here (it may not have the attributes we expect)
|
||||
if not getattr(yaml, '__with_libyaml__', False):
|
||||
from sys import version_info
|
||||
|
||||
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
|
||||
raise exc("No module named '_yaml'")
|
||||
else:
|
||||
from yaml._yaml import *
|
||||
import warnings
|
||||
warnings.warn(
|
||||
'The _yaml extension module is now located at yaml._yaml'
|
||||
' and its location is subject to change. To use the'
|
||||
' LibYAML-based parser and emitter, import from `yaml`:'
|
||||
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
|
||||
DeprecationWarning
|
||||
)
|
||||
del warnings
|
||||
# Don't `del yaml` here because yaml is actually an existing
|
||||
# namespace member of _yaml.
|
||||
|
||||
__name__ = '_yaml'
|
||||
# If the module is top-level (i.e. not a part of any specific package)
|
||||
# then the attribute should be set to ''.
|
||||
# https://docs.python.org/3.8/library/types.html
|
||||
__package__ = ''
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,13 @@
|
||||
Copyright aio-libs contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,243 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: aiohttp
|
||||
Version: 3.9.1
|
||||
Summary: Async http client/server framework (asyncio)
|
||||
Home-page: https://github.com/aio-libs/aiohttp
|
||||
Maintainer: aiohttp team <team@aiohttp.org>
|
||||
Maintainer-email: team@aiohttp.org
|
||||
License: Apache 2
|
||||
Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org
|
||||
Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org
|
||||
Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
|
||||
Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/aiohttp
|
||||
Project-URL: Docs: Changelog, https://docs.aiohttp.org/en/stable/changes.html
|
||||
Project-URL: Docs: RTD, https://docs.aiohttp.org
|
||||
Project-URL: GitHub: issues, https://github.com/aio-libs/aiohttp/issues
|
||||
Project-URL: GitHub: repo, https://github.com/aio-libs/aiohttp
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Framework :: AsyncIO
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: Apache Software License
|
||||
Classifier: Operating System :: POSIX
|
||||
Classifier: Operating System :: MacOS :: MacOS X
|
||||
Classifier: Operating System :: Microsoft :: Windows
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Topic :: Internet :: WWW/HTTP
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE.txt
|
||||
Requires-Dist: attrs >=17.3.0
|
||||
Requires-Dist: multidict <7.0,>=4.5
|
||||
Requires-Dist: yarl <2.0,>=1.0
|
||||
Requires-Dist: frozenlist >=1.1.1
|
||||
Requires-Dist: aiosignal >=1.1.2
|
||||
Requires-Dist: async-timeout <5.0,>=4.0 ; python_version < "3.11"
|
||||
Provides-Extra: speedups
|
||||
Requires-Dist: brotlicffi ; (platform_python_implementation != "CPython") and extra == 'speedups'
|
||||
Requires-Dist: Brotli ; (platform_python_implementation == "CPython") and extra == 'speedups'
|
||||
Requires-Dist: aiodns ; (sys_platform == "linux" or sys_platform == "darwin") and extra == 'speedups'
|
||||
|
||||
==================================
|
||||
Async http client/server framework
|
||||
==================================
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/aio-libs/aiohttp/master/docs/aiohttp-plain.svg
|
||||
:height: 64px
|
||||
:width: 64px
|
||||
:alt: aiohttp logo
|
||||
|
||||
|
|
||||
|
||||
.. image:: https://github.com/aio-libs/aiohttp/workflows/CI/badge.svg
|
||||
:target: https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
|
||||
:alt: GitHub Actions status for master branch
|
||||
|
||||
.. image:: https://codecov.io/gh/aio-libs/aiohttp/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/aio-libs/aiohttp
|
||||
:alt: codecov.io status for master branch
|
||||
|
||||
.. image:: https://badge.fury.io/py/aiohttp.svg
|
||||
:target: https://pypi.org/project/aiohttp
|
||||
:alt: Latest PyPI package version
|
||||
|
||||
.. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest
|
||||
:target: https://docs.aiohttp.org/
|
||||
:alt: Latest Read The Docs
|
||||
|
||||
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
|
||||
:target: https://matrix.to/#/%23aio-libs:matrix.org
|
||||
:alt: Matrix Room — #aio-libs:matrix.org
|
||||
|
||||
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
|
||||
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
|
||||
:alt: Matrix Space — #aio-libs-space:matrix.org
|
||||
|
||||
|
||||
Key Features
|
||||
============
|
||||
|
||||
- Supports both client and server side of HTTP protocol.
|
||||
- Supports both client and server Web-Sockets out-of-the-box and avoids
|
||||
Callback Hell.
|
||||
- Provides Web-server with middleware and pluggable routing.
|
||||
|
||||
|
||||
Getting started
|
||||
===============
|
||||
|
||||
Client
|
||||
------
|
||||
|
||||
To get something from the web:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get('http://python.org') as response:
|
||||
|
||||
print("Status:", response.status)
|
||||
print("Content-type:", response.headers['content-type'])
|
||||
|
||||
html = await response.text()
|
||||
print("Body:", html[:15], "...")
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
This prints:
|
||||
|
||||
.. code-block::
|
||||
|
||||
Status: 200
|
||||
Content-type: text/html; charset=utf-8
|
||||
Body: <!doctype html> ...
|
||||
|
||||
Coming from `requests <https://requests.readthedocs.io/>`_ ? Read `why we need so many lines <https://aiohttp.readthedocs.io/en/latest/http_request_lifecycle.html>`_.
|
||||
|
||||
Server
|
||||
------
|
||||
|
||||
An example using a simple server:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# examples/server_simple.py
|
||||
from aiohttp import web
|
||||
|
||||
async def handle(request):
|
||||
name = request.match_info.get('name', "Anonymous")
|
||||
text = "Hello, " + name
|
||||
return web.Response(text=text)
|
||||
|
||||
async def wshandle(request):
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(request)
|
||||
|
||||
async for msg in ws:
|
||||
if msg.type == web.WSMsgType.text:
|
||||
await ws.send_str("Hello, {}".format(msg.data))
|
||||
elif msg.type == web.WSMsgType.binary:
|
||||
await ws.send_bytes(msg.data)
|
||||
elif msg.type == web.WSMsgType.close:
|
||||
break
|
||||
|
||||
return ws
|
||||
|
||||
|
||||
app = web.Application()
|
||||
app.add_routes([web.get('/', handle),
|
||||
web.get('/echo', wshandle),
|
||||
web.get('/{name}', handle)])
|
||||
|
||||
if __name__ == '__main__':
|
||||
web.run_app(app)
|
||||
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
https://aiohttp.readthedocs.io/
|
||||
|
||||
|
||||
Demos
|
||||
=====
|
||||
|
||||
https://github.com/aio-libs/aiohttp-demos
|
||||
|
||||
|
||||
External links
|
||||
==============
|
||||
|
||||
* `Third party libraries
|
||||
<http://aiohttp.readthedocs.io/en/latest/third_party.html>`_
|
||||
* `Built with aiohttp
|
||||
<http://aiohttp.readthedocs.io/en/latest/built_with.html>`_
|
||||
* `Powered by aiohttp
|
||||
<http://aiohttp.readthedocs.io/en/latest/powered_by.html>`_
|
||||
|
||||
Feel free to make a Pull Request for adding your link to these pages!
|
||||
|
||||
|
||||
Communication channels
|
||||
======================
|
||||
|
||||
*aio-libs Discussions*: https://github.com/aio-libs/aiohttp/discussions
|
||||
|
||||
*gitter chat* https://gitter.im/aio-libs/Lobby
|
||||
|
||||
We support `Stack Overflow
|
||||
<https://stackoverflow.com/questions/tagged/aiohttp>`_.
|
||||
Please add *aiohttp* tag to your question there.
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
- async-timeout_
|
||||
- attrs_
|
||||
- multidict_
|
||||
- yarl_
|
||||
- frozenlist_
|
||||
|
||||
Optionally you may install the aiodns_ library (highly recommended for sake of speed).
|
||||
|
||||
.. _aiodns: https://pypi.python.org/pypi/aiodns
|
||||
.. _attrs: https://github.com/python-attrs/attrs
|
||||
.. _multidict: https://pypi.python.org/pypi/multidict
|
||||
.. _frozenlist: https://pypi.org/project/frozenlist/
|
||||
.. _yarl: https://pypi.python.org/pypi/yarl
|
||||
.. _async-timeout: https://pypi.python.org/pypi/async_timeout
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
``aiohttp`` is offered under the Apache 2 license.
|
||||
|
||||
|
||||
Keepsafe
|
||||
========
|
||||
|
||||
The aiohttp community would like to thank Keepsafe
|
||||
(https://www.getkeepsafe.com) for its support in the early days of
|
||||
the project.
|
||||
|
||||
|
||||
Source code
|
||||
===========
|
||||
|
||||
The latest developer version is available in a GitHub repository:
|
||||
https://github.com/aio-libs/aiohttp
|
||||
|
||||
Benchmarks
|
||||
==========
|
||||
|
||||
If you are interested in efficiency, the AsyncIO community maintains a
|
||||
list of benchmarks on the official wiki:
|
||||
https://github.com/python/asyncio/wiki/Benchmarks
|
||||
120
venv/lib/python3.12/site-packages/aiohttp-3.9.1.dist-info/RECORD
Normal file
120
venv/lib/python3.12/site-packages/aiohttp-3.9.1.dist-info/RECORD
Normal file
@@ -0,0 +1,120 @@
|
||||
aiohttp-3.9.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
aiohttp-3.9.1.dist-info/LICENSE.txt,sha256=n4DQ2311WpQdtFchcsJw7L2PCCuiFd3QlZhZQu2Uqes,588
|
||||
aiohttp-3.9.1.dist-info/METADATA,sha256=62Q_RgoSLj5AlXdi63xBFYgkT11mgnluERhDIRpWHjY,7357
|
||||
aiohttp-3.9.1.dist-info/RECORD,,
|
||||
aiohttp-3.9.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
aiohttp-3.9.1.dist-info/WHEEL,sha256=vJMp7mUkE-fMIYyE5xJ9Q2cYPnWVgHf20clVdwMSXAg,152
|
||||
aiohttp-3.9.1.dist-info/top_level.txt,sha256=iv-JIaacmTl-hSho3QmphcKnbRRYx1st47yjz_178Ro,8
|
||||
aiohttp/.hash/_cparser.pxd.hash,sha256=hYa9Vje-oMs2eh_7MfCPOh2QW_1x1yCjcZuc7AmwLd0,121
|
||||
aiohttp/.hash/_find_header.pxd.hash,sha256=_mbpD6vM-CVCKq3ulUvsOAz5Wdo88wrDzfpOsMQaMNA,125
|
||||
aiohttp/.hash/_helpers.pyi.hash,sha256=Ew4BZDc2LqFwszgZZUHHrJvw5P8HBhJ700n1Ntg52hE,121
|
||||
aiohttp/.hash/_helpers.pyx.hash,sha256=5JQ6BlMBE4HnRaCGdkK9_wpL3ZSWpU1gyLYva0Wwx2c,121
|
||||
aiohttp/.hash/_http_parser.pyx.hash,sha256=IRBIywLdT4-0kqWhb0g0WPjh6Gu10TreFmLI8JQF-L8,125
|
||||
aiohttp/.hash/_http_writer.pyx.hash,sha256=3Qg3T3D-Ud73elzPHBufK0yEu9tP5jsu6g-aPKQY9gE,125
|
||||
aiohttp/.hash/_websocket.pyx.hash,sha256=M97f-Yti-4vnE4GNTD1s_DzKs-fG_ww3jle6EUvixnE,123
|
||||
aiohttp/.hash/hdrs.py.hash,sha256=2oEszMWjYFTHoF2w4OcFCoM7osv4vY9KLLJCu9HP0xI,116
|
||||
aiohttp/__init__.py,sha256=EnBN-3iIseCzm7llWOVNSbPpNTarRN1dF2SSgRKtB-g,7782
|
||||
aiohttp/__pycache__/__init__.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/abc.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/base_protocol.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client_exceptions.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client_proto.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client_reqrep.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client_ws.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/compression_utils.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/connector.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/cookiejar.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/formdata.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/hdrs.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/helpers.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/http.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/http_exceptions.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/http_parser.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/http_websocket.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/http_writer.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/locks.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/log.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/multipart.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/payload.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/payload_streamer.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/pytest_plugin.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/resolver.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/streams.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/tcp_helpers.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/test_utils.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/tracing.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/typedefs.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_app.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_exceptions.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_fileresponse.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_log.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_middlewares.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_protocol.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_request.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_response.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_routedef.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_runner.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_server.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_urldispatcher.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_ws.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/worker.cpython-312.pyc,,
|
||||
aiohttp/_cparser.pxd,sha256=8jGIg-VJ9p3llwCakUYDsPGxA4HiZe9dmK9Jmtlz-5g,4318
|
||||
aiohttp/_find_header.pxd,sha256=0GfwFCPN2zxEKTO1_MA5sYq2UfzsG8kcV3aTqvwlz3g,68
|
||||
aiohttp/_headers.pxi,sha256=n701k28dVPjwRnx5j6LpJhLTfj7dqu2vJt7f0O60Oyg,2007
|
||||
aiohttp/_helpers.cpython-312-x86_64-linux-gnu.so,sha256=xQukDyoc-AzBs0yYHDXWrw8ICGrg8fQVexTfVBsICt0,613288
|
||||
aiohttp/_helpers.pyi,sha256=ZoKiJSS51PxELhI2cmIr5737YjjZcJt7FbIRO3ym1Ss,202
|
||||
aiohttp/_helpers.pyx,sha256=XeLbNft5X_4ifi8QB8i6TyrRuayijMSO3IDHeSA89uM,1049
|
||||
aiohttp/_http_parser.cpython-312-x86_64-linux-gnu.so,sha256=ZvlhyVbDbz3UHTWTPeqmGsn1-cW0syQ4rgr3vD3ftrI,2788720
|
||||
aiohttp/_http_parser.pyx,sha256=fzKwwVlcGnGVeiGOzo05d-2Rccqtl9-PzYKqgK3fxdI,28058
|
||||
aiohttp/_http_writer.cpython-312-x86_64-linux-gnu.so,sha256=NgI7lQoig5xSvNGFFdHI3ZJpS2emu9zj78yOe1LgYKE,503128
|
||||
aiohttp/_http_writer.pyx,sha256=aIHAp8g4ZV5kbGRdmZce-vXjELw2M6fGKyJuOdgYQqw,4575
|
||||
aiohttp/_websocket.cpython-312-x86_64-linux-gnu.so,sha256=c3irDrlStRTO9xfB94T2KQkO_17dMYNZ-9-JkufCH6Q,278168
|
||||
aiohttp/_websocket.pyx,sha256=1XuOSNDCbyDrzF5uMA2isqausSs8l2jWTLDlNDLM9Io,1561
|
||||
aiohttp/abc.py,sha256=nAyCo7BadpvvExxO1khNYqDpYt40Qp1zFmepDfZqq28,5540
|
||||
aiohttp/base_protocol.py,sha256=5JUyuIGwKf7sFhf0YLAnk36_hkSIxBnP4hN09RdMGGk,2741
|
||||
aiohttp/client.py,sha256=6s0n3HM4CRk4Gl7f-umGBTGp2MiVA7yjMnGLhSkxZXs,46918
|
||||
aiohttp/client_exceptions.py,sha256=4NmjMG2-P__buR9xfuz8_w0pvbXzr2oyuo62eQxsea8,9445
|
||||
aiohttp/client_proto.py,sha256=eHQjoiZVvm1m31Vcj1H2huV-zwHZwEl71km5l-p22aE,8624
|
||||
aiohttp/client_reqrep.py,sha256=xfJsqTzkfmRfAzChxp1za6NcfU2GDo1XXkEhTnJDik4,39756
|
||||
aiohttp/client_ws.py,sha256=nNrwu1wA0U3B0cNsVr61QfV2S60bbKfaZXHfW7klFl4,11010
|
||||
aiohttp/compression_utils.py,sha256=GCkBNJqrybMhiTQGwqqhORnaTLpRFZD_-UvRtnZ5lEQ,5015
|
||||
aiohttp/connector.py,sha256=YnUCuZQSgseKsUbo2jJH7mi0nhZD_8A6UQ5Ll3GS37k,52834
|
||||
aiohttp/cookiejar.py,sha256=PdvsOiDasDYYUOPaaAfuuFJzR4CJyHHjut02YiZ_N8M,14015
|
||||
aiohttp/formdata.py,sha256=q2gpeiM9NFsl_eSFVxHZ7Qez6RbM8_BujERMkooQkx0,6106
|
||||
aiohttp/hdrs.py,sha256=uzn5agn_jXid2h-ky6Y0ZAQ8BrPeTGLDGr-weiMctso,4613
|
||||
aiohttp/helpers.py,sha256=hYm60xCxbJCdtdtLhTt7uspQ_9HPT27gTBx2q9Fu1Zk,30255
|
||||
aiohttp/http.py,sha256=8o8j8xH70OWjnfTWA9V44NR785QPxEPrUtzMXiAVpwc,1842
|
||||
aiohttp/http_exceptions.py,sha256=7LOFFUwq04fZsnZA-NP5nukd6c2i8daM8-ejj3ndbSQ,2716
|
||||
aiohttp/http_parser.py,sha256=99kVO47hO22HQV0B4Y-1XhtOK4LTISOOs2d8c9yOGqQ,35166
|
||||
aiohttp/http_websocket.py,sha256=5qBvfvbt6f24AptTHud-T99LEnpwbGQNLMB8wGcfs9c,26704
|
||||
aiohttp/http_writer.py,sha256=fxpyRj_S3WcBl9fxxF05t8YYAUA-0jW5b_PjVSluT3Y,5933
|
||||
aiohttp/locks.py,sha256=wRYFo1U82LwBBdqwU24JEPaoTAlKaaJd2FtfDKhkTb4,1136
|
||||
aiohttp/log.py,sha256=BbNKx9e3VMIm0xYjZI0IcBBoS7wjdeIeSaiJE7-qK2g,325
|
||||
aiohttp/multipart.py,sha256=rDZYg-I-530nIEq3-U4DF2Q-fl0E7mk5YEmejzT1bak,32492
|
||||
aiohttp/payload.py,sha256=IV5HwxYqgUVY_SiyPjzUQ_YUIYvQaGkrmLmZlvSzDeM,13582
|
||||
aiohttp/payload_streamer.py,sha256=eAS8S-UWfLkEMavRjP2Uu9amC3PnbV79wHTNDoRmYn8,2087
|
||||
aiohttp/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7
|
||||
aiohttp/pytest_plugin.py,sha256=3IwpuxtFiUVFGS_ZitWuqvECSGgXQWvCW312B2TaVLY,11605
|
||||
aiohttp/resolver.py,sha256=8peXjB482v0hg1ESn87op6f-UeLXk_fAMxQo_23Ek6M,5070
|
||||
aiohttp/streams.py,sha256=vNCy0k5R7XlnSfsyTEQAkBD4Q9tNZgNm76Gqlw8Tjok,20836
|
||||
aiohttp/tcp_helpers.py,sha256=BSadqVWaBpMFDRWnhaaR941N9MiDZ7bdTrxgCb0CW-M,961
|
||||
aiohttp/test_utils.py,sha256=t5ibahuV6klkUMLdWuJhlSvzwtIRprWZqnkdHt5XJYU,20205
|
||||
aiohttp/tracing.py,sha256=Kz9u3YGTegGebYM2EMhG9RKT6-ABcsHtM7J-Qvwyus8,15152
|
||||
aiohttp/typedefs.py,sha256=8pDFTXt-5sYdzE4JsRH6UjAQyURnfZ0ueeEtgQccQZU,1491
|
||||
aiohttp/web.py,sha256=HFTQaoYVK5pM3YmxNJtZl9fGrRIdFs_Nhloxe7_lJj0,19263
|
||||
aiohttp/web_app.py,sha256=sUqIpip4BUL_pwc-Qs7IWDrFpeBNpaD8ocKFGfs9B0U,18351
|
||||
aiohttp/web_exceptions.py,sha256=7nIuiwhZ39vJJ9KrWqArA5QcWbUdqkz2CLwEpJapeN8,10360
|
||||
aiohttp/web_fileresponse.py,sha256=VRmCEr-qz7hzBy1TPa1wzid2DkgtQdIDjA18amkmRO4,10705
|
||||
aiohttp/web_log.py,sha256=DOfOxGyh2U7K5K_w6O7ILdfGcs4qOdzHxOwj2-k3c6c,7801
|
||||
aiohttp/web_middlewares.py,sha256=imxf1cfCKvfkv_jQLfTNNs_hA95oLnapRcPl-2aDsdA,4052
|
||||
aiohttp/web_protocol.py,sha256=REP-s4onglMYW2XZ5nqPCWkK0vt_2f8FiP4MFQXdd3A,23064
|
||||
aiohttp/web_request.py,sha256=TGTsWNNpGSL49Q-uV101ecX9hLZMm5PFs4mLxGkbkHc,28776
|
||||
aiohttp/web_response.py,sha256=t8mNgT5nddHLpsgZhUkqbq0NDsipmdGzfZUB5pCP_Yo,27749
|
||||
aiohttp/web_routedef.py,sha256=EFk3v1dcFnLimTT5z0JSBO3PShF0w9sIzfK9iJd-LNs,6152
|
||||
aiohttp/web_runner.py,sha256=AM3klOcr72AZVGG9-LjYo8vJdHBWgNzDLsmi_aoKfAU,11736
|
||||
aiohttp/web_server.py,sha256=5P-9uPCoPEDkK9ILbvEXmkkJWPhnTxBzdwAXwveyyDk,2587
|
||||
aiohttp/web_urldispatcher.py,sha256=sblBVycAhKVFPRbtfbZGsrPivLL0sskeE3LRPK2Deec,39557
|
||||
aiohttp/web_ws.py,sha256=eGjrE3_lUbv9kpYZluZFvdCfvahi5O4-fF7hWgyEHQk,18039
|
||||
aiohttp/worker.py,sha256=bkozEd2rAzQS0qs4knnnplOmaZ4TNdYtqWXSXx9djEc,7965
|
||||
@@ -0,0 +1,6 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.42.0)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp312-cp312-manylinux_2_17_x86_64
|
||||
Tag: cp312-cp312-manylinux2014_x86_64
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
aiohttp
|
||||
@@ -0,0 +1 @@
|
||||
f2318883e549f69de597009a914603b0f1b10381e265ef5d98af499ad973fb98 /home/runner/work/aiohttp/aiohttp/aiohttp/_cparser.pxd
|
||||
@@ -0,0 +1 @@
|
||||
d067f01423cddb3c442933b5fcc039b18ab651fcec1bc91c577693aafc25cf78 /home/runner/work/aiohttp/aiohttp/aiohttp/_find_header.pxd
|
||||
@@ -0,0 +1 @@
|
||||
6682a22524b9d4fc442e123672622be7bdfb6238d9709b7b15b2113b7ca6d52b /home/runner/work/aiohttp/aiohttp/aiohttp/_helpers.pyi
|
||||
@@ -0,0 +1 @@
|
||||
5de2db35fb795ffe227e2f1007c8ba4f2ad1b9aca28cc48edc80c779203cf6e3 /home/runner/work/aiohttp/aiohttp/aiohttp/_helpers.pyx
|
||||
@@ -0,0 +1 @@
|
||||
7f32b0c1595c1a71957a218ece8d3977ed9171caad97df8fcd82aa80addfc5d2 /home/runner/work/aiohttp/aiohttp/aiohttp/_http_parser.pyx
|
||||
@@ -0,0 +1 @@
|
||||
6881c0a7c838655e646c645d99971efaf5e310bc3633a7c62b226e39d81842ac /home/runner/work/aiohttp/aiohttp/aiohttp/_http_writer.pyx
|
||||
@@ -0,0 +1 @@
|
||||
d57b8e48d0c26f20ebcc5e6e300da2b2a6aeb12b3c9768d64cb0e53432ccf48a /home/runner/work/aiohttp/aiohttp/aiohttp/_websocket.pyx
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user