first commit

This commit is contained in:
unknown
2025-12-08 21:35:55 +09:00
commit f343f405f7
5357 changed files with 923703 additions and 0 deletions

43
.env Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

275
app/api/admin.py Normal file
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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"
}

Binary file not shown.

Binary file not shown.

43
app/models/audit_log.py Normal file
View 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
View 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
View 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})>"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

71
app/schemas/admin.py Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

View 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()

View 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()

View 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()

View 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()

Binary file not shown.

Binary file not shown.

Binary file not shown.

31
app/utils/exceptions.py Normal file
View 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
View 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
View 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
View 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
View 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

View 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

Binary file not shown.

247
venv/bin/Activate.ps1 Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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())

View 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 */

View File

@@ -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.

View File

@@ -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>`_.

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
sqlalchemy

View 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__ = ''

View File

@@ -0,0 +1 @@
pip

View File

@@ -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.

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -0,0 +1 @@
aiohttp

View File

@@ -0,0 +1 @@
f2318883e549f69de597009a914603b0f1b10381e265ef5d98af499ad973fb98 /home/runner/work/aiohttp/aiohttp/aiohttp/_cparser.pxd

View File

@@ -0,0 +1 @@
d067f01423cddb3c442933b5fcc039b18ab651fcec1bc91c577693aafc25cf78 /home/runner/work/aiohttp/aiohttp/aiohttp/_find_header.pxd

View File

@@ -0,0 +1 @@
6682a22524b9d4fc442e123672622be7bdfb6238d9709b7b15b2113b7ca6d52b /home/runner/work/aiohttp/aiohttp/aiohttp/_helpers.pyi

View File

@@ -0,0 +1 @@
5de2db35fb795ffe227e2f1007c8ba4f2ad1b9aca28cc48edc80c779203cf6e3 /home/runner/work/aiohttp/aiohttp/aiohttp/_helpers.pyx

View File

@@ -0,0 +1 @@
7f32b0c1595c1a71957a218ece8d3977ed9171caad97df8fcd82aa80addfc5d2 /home/runner/work/aiohttp/aiohttp/aiohttp/_http_parser.pyx

View File

@@ -0,0 +1 @@
6881c0a7c838655e646c645d99971efaf5e310bc3633a7c62b226e39d81842ac /home/runner/work/aiohttp/aiohttp/aiohttp/_http_writer.pyx

View File

@@ -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