diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1dd66c..39703ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,7 @@ jobs: - name: Test run: pytest --cov=app --cov-report=term-missing -v env: + TEST_DATABASE_URL: postgresql+asyncpg://test:test@localhost:5432/test_db POSTGRES_HOST: localhost POSTGRES_PORT: 5432 POSTGRES_USER: test @@ -51,4 +52,6 @@ jobs: POSTGRES_DB: test_db REDIS_HOST: localhost REDIS_PORT: 6379 + TEST_REDIS_HOST: localhost + TEST_REDIS_PORT: 6379 ADMIN_KEY: ci-admin-key diff --git a/README.md b/README.md index df4d05c..f65b24b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # RoboSense API +**배포 주소**: http://3.39.87.132 + ## 부제 ``` High-Performance Sensor Data Processing API for Robotics @@ -96,9 +98,12 @@ RobosenseAPI/ │ ├── main.py # FastAPI 앱 (랜딩페이지 서빙 포함) │ ├── database.py # DB 연결 및 세션 관리 │ ├── redis_client.py # Redis 클라이언트 -│ ├── context.py # 컨텍스트 관리 -│ ├── middleware.py # 미들웨어 -│ ├── logging_config.py # 로깅 설정 +│ ├── auth.py # API Key 생성 및 검증 +│ ├── context.py # ContextVar 기반 request_id 관리 +│ ├── exceptions.py # 커스텀 예외 클래스 +│ ├── metrics.py # 요청 지표 수집 +│ ├── middleware.py # Request ID 주입, 응답시간 측정 +│ ├── logging_config.py # request_id 로그 필터 │ ├── models/ │ │ ├── __init__.py │ │ ├── db_models.py # SQLAlchemy ORM 모델 @@ -107,9 +112,11 @@ RobosenseAPI/ │ │ └── enum.py # Enum 타입 정의 │ ├── routes/ │ │ ├── __init__.py -│ │ ├── sensor_routes.py # 센서 데이터 API +│ │ ├── sensor_routes.py # 센서 데이터 API + 배치 워커 │ │ ├── robot_routes.py # 로봇 관리 API -│ │ └── stats_routes.py # 통계 API +│ │ ├── stats_routes.py # 통계 API +│ │ ├── admin_routes.py # API Key 발급/폐기 (관리자 전용) +│ │ └── demo_routes.py # 랜딩페이지 공개 조회 (인증 불필요) │ └── utils/ │ └── retry.py # 재시도 유틸리티 ├── cpp_modules/ # C++ 연산 모듈 diff --git a/app/routes/admin_routes.py b/app/routes/admin_routes.py index 31c4878..dc831c3 100644 --- a/app/routes/admin_routes.py +++ b/app/routes/admin_routes.py @@ -1,6 +1,8 @@ from fastapi import APIRouter, Depends, status +from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from typing import Optional from app.database import get_db from app.models.db_models import ApiKey @@ -9,14 +11,18 @@ router = APIRouter(prefix="/admin", tags=["관리"]) +class ApiKeyCreate(BaseModel): + robot_id: Optional[int] = None + + @router.post("/api-keys", status_code=status.HTTP_201_CREATED) async def issue_api_key( - body: dict = None, + body: ApiKeyCreate = ApiKeyCreate(), admin_key: str = Depends(verify_admin_key), db: AsyncSession = Depends(get_db), ): """API Key 발급. 평문 키는 이 응답에서만 확인 가능.""" - robot_id = body.get("robot_id") if body else None + robot_id = body.robot_id plain_key, salt, key_hash = generate_api_key() api_key = ApiKey(key_hash=key_hash, salt=salt, robot_id=robot_id) diff --git a/app/routes/sensor_routes.py b/app/routes/sensor_routes.py index f8152a1..6d1bc0a 100644 --- a/app/routes/sensor_routes.py +++ b/app/routes/sensor_routes.py @@ -9,7 +9,6 @@ from app.auth import verify_api_key from app.exceptions import SensorNotFoundError import logging -import time import asyncio import json import math @@ -19,14 +18,12 @@ router = APIRouter() logger = logging.getLogger(__name__) -last_invalidation = {} sensor_queue = asyncio.Queue() @router.post('/api/sensors', status_code=status.HTTP_201_CREATED) async def collect_sensor_data(data: SensorDataCreate, _key=Depends(verify_api_key)): redis = get_redis() - now = time.time() sensors_to_add = [ SensorData( @@ -52,13 +49,13 @@ async def collect_sensor_data(data: SensorDataCreate, _key=Depends(verify_api_ke await redis.ltrim(key, 0, 4) await redis.expire(key, 10) - if now - last_invalidation.get(data.robot_id, 0) > 10: + # 무효화 스로틀은 Redis로 공유. 프로세스 로컬 dict는 replica마다 따로 놀아 + # "10초에 1번"이 프로세스 수만큼 중복 무효화됨. SETNX로 윈도우를 선점한 1개만 삭제. + if await redis.set(f"invalidate:robot:{data.robot_id}:detail", "1", nx=True, ex=10): await redis.delete(f"robot:{data.robot_id}:detail") - last_invalidation[data.robot_id] = now - if now - last_invalidation.get('stats', 0) > 60: + if await redis.set("invalidate:stats", "1", nx=True, ex=60): await redis.delete("stats:recent") - last_invalidation['stats'] = now return { "status": "success", @@ -79,6 +76,12 @@ async def check_filter_data( _key=Depends(verify_api_key)): query = select(SensorData).order_by(SensorData.id.desc()) + # tz 없는 입력은 UTC로 간주 — TIMESTAMPTZ 컬럼과 비교 시 타임존 어긋남 방지 + if start_time and start_time.tzinfo is None: + start_time = start_time.replace(tzinfo=timezone.utc) + if end_time and end_time.tzinfo is None: + end_time = end_time.replace(tzinfo=timezone.utc) + if robot_id: query = query.where(SensorData.robot_id == robot_id) if sensor_type: diff --git a/index.html b/index.html index 8d36c6b..c78a522 100644 --- a/index.html +++ b/index.html @@ -292,13 +292,13 @@
Registered Robots
function checkServer(){var d=document.getElementById('statusDot'),t=document.getElementById('statusText');fetch(B+'/health_check').then(function(r){if(r.ok){d.className='status-dot online';t.textContent='Server Online';}else{d.className='status-dot offline';t.textContent='Unhealthy';}}).catch(function(){d.className='status-dot offline';t.textContent='Server Offline';});} checkServer(); function toggle(h){var b=h.nextElementSibling,t=h.querySelector('.endpoint-toggle');b.classList.toggle('open');t.style.transform=b.classList.contains('open')?'rotate(180deg)':'';} -function showResp(id,s,d,m){var b=document.getElementById(id),ok=s>=200&&s<300;b.className='response-box visible';b.innerHTML='
'+s+''+(ok?'Success':'Error')+''+m+'ms
'+(typeof d==='string'?d:JSON.stringify(d,null,2))+'
';} +function showResp(id,s,d,m){var b=document.getElementById(id),ok=s>=200&&s<300;b.className='response-box visible';b.innerHTML='
'+(ok?'Success':'Error')+'
';b.querySelector('.status-code').textContent=s;b.querySelector('.response-time').textContent=m+'ms';b.querySelector('.response-body-output').textContent=(typeof d==='string'?d:JSON.stringify(d,null,2));} function showLoad(id){var b=document.getElementById(id);b.className='response-box visible';b.innerHTML='
Sending...
';} -function showErr(id,m){var b=document.getElementById(id);b.className='response-box visible';b.innerHTML='
Error
'+m+'
';} +function showErr(id,m){var b=document.getElementById(id);b.className='response-box visible';b.innerHTML='
Error
';b.querySelector('.response-body-output').textContent=m;} function api(method,path,body){var s=performance.now(),o={method:method,headers:{'Content-Type':'application/json'}};if(body)o.body=JSON.stringify(body);return fetch(B+path,o).then(function(r){var m=Math.round(performance.now()-s);return r.json().then(function(d){return{status:r.status,data:d,ms:m};}).catch(function(){return r.text().then(function(t){return{status:r.status,data:t,ms:m};});});});} function unlock(){['lock-post-sensor','lock-get-sensor','lock-filter-sensor','lock-stats'].forEach(function(id){document.getElementById(id).classList.add('unlocked');});} function lock(){['lock-post-sensor','lock-get-sensor','lock-filter-sensor','lock-stats'].forEach(function(id){document.getElementById(id).classList.remove('unlocked');});} -function updateRL(){var el=document.getElementById('registeredRobots');if(!R.length){el.innerHTML='아직 등록된 로봇이 없습니다.';return;}el.innerHTML=R.map(function(r){return'
#'+r.id+''+r.name+'
';}).join('');} +function updateRL(){var el=document.getElementById('registeredRobots');if(!R.length){el.innerHTML='아직 등록된 로봇이 없습니다.';return;}el.innerHTML='';R.forEach(function(r){var chip=document.createElement('div');chip.className='robot-chip';var idSpan=document.createElement('span');idSpan.className='chip-id';idSpan.textContent='#'+r.id;var nameSpan=document.createElement('span');nameSpan.className='chip-name';nameSpan.textContent=r.name;chip.appendChild(idSpan);chip.appendChild(nameSpan);el.appendChild(chip);});} var MODELS=['Warehouse-Bot-v2','Patrol-Bot-X1','Cargo-Mover-3K','Scout-Mini','HeavyLift-900','SpeedRunner-S','Inspector-Pro','CleanBot-Z','Picker-Alpha','Navigator-V5']; var autoTimer=null,autoSentN=0,autoErrN=0,autoTotalMs=0; diff --git a/landingpage.html b/landingpage.html deleted file mode 100644 index 16004bd..0000000 --- a/landingpage.html +++ /dev/null @@ -1,782 +0,0 @@ - - - - - -RoboSense API — Landing Page - - - - - -
-
- High-Performance Sensor Data API -
-

RoboSense API

-

물류 창고 자율주행 로봇을 위한 고성능 센서 데이터 처리 API.
- 실시간 수집 · C++ 최적화 · Redis 캐싱 · 비동기 배치 처리

-
- API 둘러보기 ↓ - GitHub ↗ -
-
- - -
-
Architecture
-
시스템 아키텍처
-

Nginx 리버스 프록시 뒤에 다중 FastAPI 인스턴스가 비동기로 동작하며, Redis 캐싱과 PostgreSQL을 통해 높은 처리량과 낮은 지연 시간을 달성합니다.

-
-
Robots센서 데이터 전송
-
-
Nginx로드 밸런서
-
-
FastAPI ×N비동기 처리
-
-
Redis캐시 / TTL
-
-
PostgreSQL영구 저장
-
- - -
- -
기술 스택
-
-
-

FastAPI + Uvicorn

비동기 ASGI 서버, 타입 힌트 기반 자동 문서화

-

PostgreSQL 16

asyncpg 드라이버, JSONB 센서 데이터, 복합 인덱스

-

Redis 7

Cache-Aside 패턴, TTL 기반 자동 만료, 캐시 워밍

-

C++ 17 + pybind11

센서 직렬화, 이동평균 필터링, null 비율 계산

-

Nginx

리버스 프록시, 로드 밸런싱, GZip 압축

-

Docker Compose

멀티 컨테이너 오케스트레이션, 환경 일관성

-

AWS EC2

실제 배포 환경에서 TPS 부하 테스트

-

Locust

점진적 부하 테스트 (100 → 5,000 동시 접속)

-
-
- - -
-
Usage Flow
-
API 사용 흐름
-

모든 센서 데이터 및 통계 기능은 로봇 등록 후 사용할 수 있습니다.

- -
-
-
Step 1 필수 — 센서 데이터 수집, 필터링, 통계 등 모든 기능은 로봇 등록(POST /api/robots) 이후에만 사용할 수 있습니다.
-
- -
-
-
1
-

로봇 등록

-

POST /api/robots

-
-
-
-
2
-

센서 데이터 수집

-

POST /api/sensors

-
-
-
-
3
-

조회 & 필터링

-

GET /api/sensors/*

-
-
-
-
4
-

통계 확인

-

GET /api/stats

-
-
- - -
-
로봇 관리
- -
-
- POST - /api/robots - 로봇 등록 (최초 필수) - -
-
-
Request Body
- - - - - - -
FieldTypeNote
name 필수string로봇 이름 (중복 불가)
modelstring로봇 모델명
statusenumactive / inactive / maintenance
battery_levelint0 ~ 100
-
Response 201
-
{
-  "id": 1,
-  "name": "Robot-A",
-  "model": "Warehouse-Bot-v2",
-  "status": "active",
-  "battery_level": 100,
-  "last_seen": null,
-  "created_at": "2026-03-21T09:00:00"
-}
-
-
- -
-
- GET - /api/robots - 전체 로봇 목록 (상태 필터) - -
-
-
Query Parameters
- - - -
ParamTypeNote
statusstringactive / inactive / maintenance
-
Response 200
-
[
-  { "id": 1, "name": "Robot-A", "status": "active", ... },
-  { "id": 2, "name": "Robot-B", "status": "inactive", ... }
-]
-
-
- -
-
- GET - /api/robots/{id} - 특정 로봇 + 최근 센서 데이터 - -
-
-
Response 200
-
{
-  "id": 1,
-  "name": "Robot-A",
-  "model": "Warehouse-Bot-v2",
-  "status": "active",
-  "battery_level": 85,
-  "recent_sensors": [
-    { "sensor_type": "IMU", "timestamp": "...", "raw_data": {...} },
-    { "sensor_type": "GPS", "timestamp": "...", "raw_data": null }
-  ]
-}
-
-
- -
-
- PUT - /api/robots/{id} - 로봇 상태 업데이트 - -
-
-
Request Body
- - - - -
FieldTypeNote
status 필수enumactive / inactive / maintenance
battery_level 필수int0 ~ 100
-
-
-
- - -
-
센서 데이터
- -
-
- POST - /api/sensors - 센서 데이터 수집 (비동기 배치) - -
-
-
Request Body
- - - - - - -
FieldTypeNote
robot_id 필수int등록된 로봇 ID
timestamp 필수datetime측정 시각
sensors 필수array센서 배열 (IMU, GPS, LiDAR)
sensors[].dataobject | nullnull 허용 (신호 끊김)
-
Request Example
-
{
-  "robot_id": 1,
-  "timestamp": "2026-03-21T10:30:00",
-  "sensors": [
-    { "sensor_type": "IMU", "data": {"acceleration": {"x": 1.2, "y": -0.5, "z": 9.8}} },
-    { "sensor_type": "GPS", "data": null },
-    { "sensor_type": "LiDAR", "data": {"distance": 3.5, "angle": 45} }
-  ]
-}
-
Response 201
-
{ "status": "success", "robot_id": 1, "queued_count": 3 }
-
-
- -
-
- GET - /api/sensors - 센서 데이터 목록 (커서 페이징) - -
-
-
Query Parameters
- - - - - - -
ParamTypeNote
robot_idint로봇 필터
sensor_typestringIMU / GPS / LiDAR
cursor_idint페이징 커서
limitint기본 100
-
Response 200
-
{
-  "data": [ ... ],
-  "next_cursor": 450,
-  "has_more": true
-}
-
-
- -
-
- GET - /api/sensors/filtered - 이동평균 필터링 (C++ 처리) - -
-
-
Query Parameters
- - - - - - -
ParamTypeNote
robot_id 필수int로봇 ID
sensor_type 필수stringIMU / GPS / LiDAR
field 필수string예: acceleration.x
window_size 필수int이동평균 윈도우 크기
-
Response 200
-
{
-  "robot_id": 1,
-  "sensor_type": "IMU",
-  "field": "acceleration.x",
-  "original_data": [1.2, 1.3, 1.1, 1.4, 1.0],
-  "filtered_data": [1.2, 1.2, 1.27],
-  "window_size": 3
-}
-
-
- -
-
- GET - /api/sensors/{id} - 특정 센서 데이터 조회 - -
-
-
Response 200
-
{
-  "id": 42,
-  "robot_id": 1,
-  "sensor_type": "IMU",
-  "timestamp": "2026-03-21T10:30:00",
-  "raw_data": {"acceleration": {"x": 1.2, "y": -0.5, "z": 9.8}},
-  "created_at": "2026-03-21T10:30:01"
-}
-
-
-
- - -
-
통계
- -
-
- GET - /api/stats - 종합 통계 (null 비율, 로봇 상태 요약) - -
-
-
Query Parameters
- - - - -
ParamTypeNote
start_timedatetime기본: 1시간 전
end_timedatetime기본: 현재
-
Response 200
-
{
-  "timestamp": "2026-03-21T10:30:00Z",
-  "time_range": { "start": "...", "end": "..." },
-  "null_rates": {
-    "IMU": 0.02,
-    "GPS": 0.15,
-    "LiDAR": 0.01
-  },
-  "robot_summary": {
-    "total_robots": 12,
-    "active": 10,
-    "inactive": 2,
-    "maintenance": 0,
-    "status_details": [
-      { "robot_id": 3, "status": "inactive", "last_seen": "..." }
-    ]
-  }
-}
-
-
-
- - -
-
시스템
- -
-
- GET - /health_check - DB + Redis 상태 확인 - -
-
-
Response 200 (healthy)
-
{ "status": "healthy", "db": true, "redis": true }
-
Response 503 (unhealthy)
-
{ "status": "unhealthy", "db": false, "redis": true }
-
-
-
-
- - -
-
Performance
-
성능 최적화 포인트
-

극한 조건에서의 성능 최적화를 목표로 설계되었습니다.

-
-
-
Batch
-
asyncio Queue + COPY 프로토콜
비동기 배치 커밋
-
-
-
C++
-
pybind11 연동
센서 직렬화 & 필터링
-
-
-
Cache
-
Redis Cache-Aside
TTL 10s ~ 60s 자동 갱신
-
-
-
GZip
-
응답 압축 미들웨어
1KB 이상 자동 압축
-
-
-
- - - - - - - diff --git a/profiles/flame.svg b/profiles/flame.svg new file mode 100644 index 0000000..9d892bd --- /dev/null +++ b/profiles/flame.svg @@ -0,0 +1,491 @@ +py-spy record -o /app/profiles/flame.svg --pid 1 --duration 60 Reset ZoomSearch _deliver_cancellation (anyio/_backends/_asyncio.py:589) (6 samples, 0.10%)decode (json/decoder.py:337) (10 samples, 0.17%)raw_decode (json/decoder.py:353) (9 samples, 0.15%)_json_decoder (sqlalchemy/dialects/postgresql/asyncpg.py:1194) (14 samples, 0.24%)loads (json/__init__.py:346) (14 samples, 0.24%)batch_commit_worker (app/routes/sensor_routes.py:211) (13 samples, 0.22%)copy_records_to_table (asyncpg/connection.py:1127) (11 samples, 0.19%)encode (json/encoder.py:200) (10 samples, 0.17%)iterencode (json/encoder.py:258) (8 samples, 0.14%)batch_commit_worker (app/routes/sensor_routes.py:212) (20 samples, 0.34%)<listcomp> (app/routes/sensor_routes.py:212) (20 samples, 0.34%)sensor_to_tuple (app/routes/sensor_routes.py:184) (19 samples, 0.32%)dumps (json/__init__.py:231) (11 samples, 0.19%)greenlet_spawn (sqlalchemy/util/_concurrency_py3k.py:184) (6 samples, 0.10%)_check (asyncpg/connresource.py:20) (15 samples, 0.25%)rollback (asyncpg/transaction.py:221) (13 samples, 0.22%)greenlet_spawn (sqlalchemy/util/_concurrency_py3k.py:196) (67 samples, 1.13%)_rollback_and_discard (sqlalchemy/dialects/postgresql/asyncpg.py:862) (66 samples, 1.12%)rollback (asyncpg/transaction.py:227) (47 samples, 0.80%)__rollback (asyncpg/transaction.py:206) (40 samples, 0.68%)execute (asyncpg/connection.py:354) (35 samples, 0.59%)close (sqlalchemy/ext/asyncio/session.py:1016) (86 samples, 1.46%)coro (starlette/middleware/base.py:142) (7 samples, 0.12%)__init__ (gzip.py:205) (14 samples, 0.24%)_write_gzip_header (gzip.py:246) (10 samples, 0.17%)__call__ (starlette/middleware/gzip.py:25) (52 samples, 0.88%)__init__ (starlette/middleware/gzip.py:126) (47 samples, 0.80%)__init__ (gzip.py:213) (20 samples, 0.34%)close (gzip.py:334) (18 samples, 0.30%)close (gzip.py:335) (6 samples, 0.10%)__call__ (starlette/middleware/gzip.py:129) (33 samples, 0.56%)__call__ (starlette/middleware/exceptions.py:59) (7 samples, 0.12%)__call__ (fastapi/middleware/asyncexitstack.py:16) (25 samples, 0.42%)matches (fastapi/routing.py:723) (6 samples, 0.10%)app (starlette/routing.py:733) (18 samples, 0.30%)wrapped_app (starlette/_exception_handler.py:31) (11 samples, 0.19%)__aexit__ (sqlalchemy/ext/asyncio/session.py:1070) (8 samples, 0.14%)__aexit__ (sqlalchemy/ext/asyncio/session.py:1071) (9 samples, 0.15%)get_db (app/database.py:29) (19 samples, 0.32%)__aexit__ (contextlib.py:217) (30 samples, 0.51%)__aexit__ (contextlib.py:728) (33 samples, 0.56%)app (fastapi/routing.py:102) (46 samples, 0.78%)app (fastapi/routing.py:104) (11 samples, 0.19%)_cleanup (sqlalchemy/orm/state.py:558) (6 samples, 0.10%)__aenter__ (anyio/_backends/_asyncio.py:731) (7 samples, 0.12%)__aexit__ (anyio/_backends/_asyncio.py:750) (14 samples, 0.24%)__aexit__ (anyio/_backends/_asyncio.py:789) (8 samples, 0.14%)__init__ (anyio/_backends/_asyncio.py:724) (8 samples, 0.14%)__new__ (anyio/_backends/_asyncio.py:399) (7 samples, 0.12%)create_task_group (anyio/_backends/_asyncio.py:2442) (9 samples, 0.15%)receive_or_disconnect (starlette/middleware/base.py:117) (59 samples, 1.00%)create_task_group (anyio/_core/_tasks.py:163) (12 samples, 0.20%)create_task (asyncio/tasks.py:384) (8 samples, 0.14%)_spawn (anyio/_backends/_asyncio.py:884) (12 samples, 0.20%)receive_or_disconnect (starlette/middleware/base.py:124) (30 samples, 0.51%)start_soon (anyio/_backends/_asyncio.py:904) (30 samples, 0.51%)wrap (starlette/middleware/base.py:120) (22 samples, 0.37%)wrapped_receive (starlette/middleware/base.py:84) (10 samples, 0.17%)_format_callbacks (asyncio/base_futures.py:35) (7 samples, 0.12%)format_cb (asyncio/base_futures.py:32) (7 samples, 0.12%)_task_repr_info (asyncio/base_tasks.py:10) (11 samples, 0.19%)_future_repr_info (asyncio/base_futures.py:58) (9 samples, 0.15%)_task_repr_info (asyncio/base_tasks.py:18) (11 samples, 0.19%)cancel (anyio/_backends/_asyncio.py:630) (27 samples, 0.46%)wrapper (reprlib.py:21) (25 samples, 0.42%)_task_repr (asyncio/base_tasks.py:28) (25 samples, 0.42%)receive_or_disconnect (starlette/middleware/base.py:125) (60 samples, 1.02%)wrap (starlette/middleware/base.py:121) (38 samples, 0.64%)cancel (anyio/_backends/_asyncio.py:636) (10 samples, 0.17%)stream (starlette/requests.py:228) (157 samples, 2.66%)st..body (starlette/requests.py:243) (170 samples, 2.88%)bo..app (fastapi/routing.py:373) (176 samples, 2.98%)app..app (fastapi/routing.py:376) (13 samples, 0.22%)get_content_maintype (email/message.py:628) (7 samples, 0.12%)app (fastapi/routing.py:382) (8 samples, 0.14%)decode (json/decoder.py:337) (16 samples, 0.27%)raw_decode (json/decoder.py:353) (10 samples, 0.17%)app (fastapi/routing.py:385) (24 samples, 0.41%)json (starlette/requests.py:251) (21 samples, 0.36%)loads (json/__init__.py:346) (19 samples, 0.32%)solve_dependencies (fastapi/dependencies/utils.py:619) (6 samples, 0.10%)__delitem__ (starlette/datastructures.py:609) (8 samples, 0.14%)solve_dependencies (fastapi/dependencies/utils.py:620) (18 samples, 0.30%)solve_dependencies (fastapi/dependencies/utils.py:625) (10 samples, 0.17%)__getitem__ (typing.py:1653) (6 samples, 0.10%)__init__ (starlette/datastructures.py:397) (19 samples, 0.32%)solve_dependencies (fastapi/dependencies/utils.py:685) (23 samples, 0.39%)query_params (starlette/requests.py:132) (23 samples, 0.39%)solve_dependencies (fastapi/dependencies/utils.py:691) (6 samples, 0.10%)solve_dependencies (fastapi/dependencies/utils.py:645) (44 samples, 0.75%)asynccontextmanager (contextlib.py:332) (7 samples, 0.12%)_solve_generator (fastapi/dependencies/utils.py:579) (14 samples, 0.24%)add (_weakrefset.py:88) (9 samples, 0.15%)__call__ (sqlalchemy/orm/session.py:5114) (6 samples, 0.10%)__init__ (sqlalchemy/ext/asyncio/session.py:257) (7 samples, 0.12%)__init__ (sqlalchemy/ext/asyncio/session.py:269) (7 samples, 0.12%)__init__ (weakref.py:353) (24 samples, 0.41%)enter_async_context (contextlib.py:650) (105 samples, 1.78%)e..__aenter__ (contextlib.py:210) (103 samples, 1.74%)get_db (app/database.py:29) (91 samples, 1.54%)__call__ (sqlalchemy/orm/session.py:5121) (67 samples, 1.13%)__init__ (sqlalchemy/ext/asyncio/session.py:270) (50 samples, 0.85%)__init__ (sqlalchemy/orm/session.py:1798) (30 samples, 0.51%)__setitem__ (weakref.py:167) (28 samples, 0.47%)solve_dependencies (fastapi/dependencies/utils.py:668) (123 samples, 2.08%)s.._solve_generator (fastapi/dependencies/utils.py:582) (108 samples, 1.83%)_..is_sequence_field (fastapi/_compat/main.py:207) (9 samples, 0.15%)_get_multidict_value (fastapi/dependencies/utils.py:756) (36 samples, 0.61%)is_sequence_field (fastapi/_compat/main.py:209) (26 samples, 0.44%)is_sequence_field (fastapi/_compat/v2.py:430) (26 samples, 0.44%)field_annotation_is_sequence (fastapi/_compat/shared.py:87) (22 samples, 0.37%)field_annotation_is_sequence (fastapi/_compat/shared.py:90) (18 samples, 0.30%)_annotation_is_sequence (fastapi/_compat/shared.py:80) (13 samples, 0.22%)lenient_issubclass (fastapi/_compat/shared.py:70) (13 samples, 0.22%)__subclasscheck__ (typing.py:1606) (7 samples, 0.12%)field_annotation_is_sequence (fastapi/_compat/shared.py:90) (6 samples, 0.10%)request_params_to_args (fastapi/dependencies/utils.py:815) (49 samples, 0.83%)_get_multidict_value (fastapi/dependencies/utils.py:767) (11 samples, 0.19%)is_sequence_field (fastapi/_compat/main.py:209) (8 samples, 0.14%)is_sequence_field (fastapi/_compat/v2.py:430) (8 samples, 0.14%)field_annotation_is_sequence (fastapi/_compat/shared.py:87) (7 samples, 0.12%)request_params_to_args (fastapi/dependencies/utils.py:820) (6 samples, 0.10%)request_params_to_args (fastapi/dependencies/utils.py:823) (7 samples, 0.12%)request_params_to_args (fastapi/dependencies/utils.py:843) (21 samples, 0.36%)_get_multidict_value (fastapi/dependencies/utils.py:767) (11 samples, 0.19%)is_sequence_field (fastapi/_compat/main.py:209) (8 samples, 0.14%)is_sequence_field (fastapi/_compat/v2.py:430) (8 samples, 0.14%)field_annotation_is_sequence (fastapi/_compat/shared.py:87) (7 samples, 0.12%)request_params_to_args (fastapi/dependencies/utils.py:848) (6 samples, 0.10%)_validate_value_with_model_field (fastapi/dependencies/utils.py:742) (9 samples, 0.15%)validate (fastapi/_compat/v2.py:178) (8 samples, 0.14%)validate_python (pydantic/type_adapter.py:441) (7 samples, 0.12%)request_params_to_args (fastapi/dependencies/utils.py:849) (13 samples, 0.22%)solve_dependencies (fastapi/dependencies/utils.py:687) (115 samples, 1.95%)s..solve_dependencies (fastapi/dependencies/utils.py:645) (302 samples, 5.12%)solve_.._async_ping (sqlalchemy/dialects/postgresql/asyncpg.py:824) (8 samples, 0.14%)transaction (asyncpg/connection.py:303) (6 samples, 0.10%)start (asyncpg/transaction.py:146) (22 samples, 0.37%)execute (asyncpg/connection.py:354) (21 samples, 0.36%)_async_ping (sqlalchemy/dialects/postgresql/asyncpg.py:825) (34 samples, 0.58%)_do_execute (asyncpg/connection.py:2013) (8 samples, 0.14%)_async_ping (sqlalchemy/dialects/postgresql/asyncpg.py:827) (50 samples, 0.85%)fetchrow (asyncpg/connection.py:752) (49 samples, 0.83%)_execute (asyncpg/connection.py:1873) (44 samples, 0.75%)__execute (asyncpg/connection.py:1970) (38 samples, 0.64%)_do_execute (asyncpg/connection.py:2033) (25 samples, 0.42%)<lambda> (asyncpg/connection.py:1951) (6 samples, 0.10%)_async_ping (sqlalchemy/dialects/postgresql/asyncpg.py:829) (20 samples, 0.34%)rollback (asyncpg/transaction.py:227) (15 samples, 0.25%)__rollback (asyncpg/transaction.py:206) (13 samples, 0.22%)execute (asyncpg/connection.py:354) (9 samples, 0.15%)_start_transaction (sqlalchemy/dialects/postgresql/asyncpg.py:843) (7 samples, 0.12%)start (asyncpg/transaction.py:146) (26 samples, 0.44%)execute (asyncpg/connection.py:354) (22 samples, 0.37%)_prepare_and_execute (sqlalchemy/dialects/postgresql/asyncpg.py:520) (43 samples, 0.73%)_start_transaction (sqlalchemy/dialects/postgresql/asyncpg.py:848) (33 samples, 0.56%)_prepare_and_execute (sqlalchemy/dialects/postgresql/asyncpg.py:526) (6 samples, 0.10%)_prepare_and_execute (sqlalchemy/dialects/postgresql/asyncpg.py:531) (11 samples, 0.19%)__bind_execute (asyncpg/prepared_stmt.py:268) (19 samples, 0.32%)__do_execute (asyncpg/prepared_stmt.py:257) (19 samples, 0.32%)_prepare_and_execute (sqlalchemy/dialects/postgresql/asyncpg.py:550) (29 samples, 0.49%)fetch (asyncpg/prepared_stmt.py:177) (22 samples, 0.37%)_prepare_and_execute (sqlalchemy/dialects/postgresql/asyncpg.py:551) (6 samples, 0.10%)_prepare_and_execute (sqlalchemy/dialects/postgresql/asyncpg.py:553) (14 samples, 0.24%)match (re/__init__.py:166) (13 samples, 0.22%)wait_for (asyncio/tasks.py:466) (8 samples, 0.14%)wait_for (asyncio/tasks.py:467) (9 samples, 0.15%)_ensure_future (asyncio/tasks.py:663) (7 samples, 0.12%)wait_for (asyncio/tasks.py:470) (25 samples, 0.42%)ensure_future (asyncio/tasks.py:659) (24 samples, 0.41%)_ensure_future (asyncio/tasks.py:680) (15 samples, 0.25%)greenlet_spawn (sqlalchemy/util/_concurrency_py3k.py:196) (299 samples, 5.06%)greenl..execute (sqlalchemy/ext/asyncio/session.py:449) (340 samples, 5.76%)execute..greenlet_spawn (sqlalchemy/util/_concurrency_py3k.py:203) (21 samples, 0.36%)verify_api_key (app/auth.py:39) (348 samples, 5.89%)verify_..__init__ (sqlalchemy/sql/elements.py:2000) (22 samples, 0.37%)safe_construct (sqlalchemy/sql/elements.py:5555) (11 samples, 0.19%)__new__ (sqlalchemy/sql/elements.py:5462) (9 samples, 0.15%)_bind_param (sqlalchemy/sql/elements.py:4669) (35 samples, 0.59%)expect (sqlalchemy/sql/coercions.py:396) (57 samples, 0.97%)_literal_coercion (sqlalchemy/sql/coercions.py:804) (40 samples, 0.68%)_boolean_compare (sqlalchemy/sql/default_comparator.py:120) (60 samples, 1.02%)__init__ (sqlalchemy/sql/elements.py:3947) (13 samples, 0.22%)__hash__ (sqlalchemy/sql/annotation.py:367) (11 samples, 0.19%)_boolean_compare (sqlalchemy/sql/default_comparator.py:134) (23 samples, 0.39%)_construct_for_op (sqlalchemy/sql/elements.py:2914) (21 samples, 0.36%)__eq__ (sqlalchemy/sql/annotation.py:371) (97 samples, 1.64%)__eq__ (sqlalchemy/sql/operators.py:584) (96 samples, 1.63%)operate (sqlalchemy/sql/elements.py:1535) (91 samples, 1.54%)__eq__ (sqlalchemy/sql/operators.py:584) (90 samples, 1.52%)operate (sqlalchemy/sql/type_api.py:210) (87 samples, 1.47%)__eq__ (sqlalchemy/sql/operators.py:584) (105 samples, 1.78%)_..operate (sqlalchemy/orm/attributes.py:453) (104 samples, 1.76%)__eq__ (sqlalchemy/sql/operators.py:584) (102 samples, 1.73%)operate (sqlalchemy/orm/properties.py:479) (102 samples, 1.73%)expect (sqlalchemy/sql/coercions.py:386) (7 samples, 0.12%)<listcomp> (sqlalchemy/sql/selectable.py:5404) (22 samples, 0.37%)__init__ (sqlalchemy/sql/selectable.py:5403) (27 samples, 0.46%)__init__ (sqlalchemy/sql/selectable.py:5407) (6 samples, 0.10%)select (sqlalchemy/sql/_selectable_constructors.py:538) (39 samples, 0.66%)_generative (sqlalchemy/sql/base.py:283) (6 samples, 0.10%)verify_api_key (app/auth.py:40) (162 samples, 2.74%)ve..where (<string>:2) (13 samples, 0.22%)_allrows (sqlalchemy/engine/result.py:546) (6 samples, 0.10%)_soft_close (sqlalchemy/engine/result.py:2338) (7 samples, 0.12%)_allrows (sqlalchemy/engine/result.py:548) (11 samples, 0.19%)_fetchall_impl (sqlalchemy/engine/result.py:1681) (9 samples, 0.15%)_fetchall_impl (sqlalchemy/engine/result.py:2277) (9 samples, 0.15%)all (sqlalchemy/engine/result.py:1774) (19 samples, 0.32%)verify_api_key (app/auth.py:42) (21 samples, 0.36%)solve_dependencies (fastapi/dependencies/utils.py:674) (569 samples, 9.64%)solve_dependen..verify_api_key (app/auth.py:45) (17 samples, 0.29%)hash_api_key (app/auth.py:28) (15 samples, 0.25%)_get_multidict_value (fastapi/dependencies/utils.py:756) (6 samples, 0.10%)request_params_to_args (fastapi/dependencies/utils.py:815) (8 samples, 0.14%)solve_dependencies (fastapi/dependencies/utils.py:684) (18 samples, 0.30%)_is_model_class (fastapi/_compat/main.py:359) (8 samples, 0.14%)request_body_to_args (fastapi/dependencies/utils.py:965) (18 samples, 0.30%)solve_dependencies (fastapi/dependencies/utils.py:702) (63 samples, 1.07%)request_body_to_args (fastapi/dependencies/utils.py:975) (42 samples, 0.71%)_validate_value_with_model_field (fastapi/dependencies/utils.py:742) (41 samples, 0.69%)validate (fastapi/_compat/v2.py:178) (40 samples, 0.68%)validate_python (pydantic/type_adapter.py:441) (38 samples, 0.64%)app (fastapi/routing.py:420) (1,021 samples, 17.29%)app (fastapi/routing.py:420)execute_command (redis/asyncio/client.py:725) (9 samples, 0.15%)call_with_retry (redis/asyncio/retry.py:50) (9 samples, 0.15%)_send_command_parse_response (redis/asyncio/client.py:700) (7 samples, 0.12%)parse_response (redis/asyncio/client.py:746) (7 samples, 0.12%)read_response (redis/asyncio/connection.py:607) (7 samples, 0.12%)read_response (redis/_parsers/resp2.py:82) (7 samples, 0.12%)check_filter_sensor_data (app/routes/sensor_routes.py:117) (15 samples, 0.25%)check_filter_sensor_data (app/routes/sensor_routes.py:135) (7 samples, 0.12%)loads (json/__init__.py:346) (6 samples, 0.10%)__init__ (<string>:2) (6 samples, 0.10%)set (sqlalchemy/orm/attributes.py:1281) (6 samples, 0.10%)__set__ (sqlalchemy/orm/attributes.py:540) (18 samples, 0.30%)set (sqlalchemy/orm/attributes.py:1290) (10 samples, 0.17%)<listcomp> (app/routes/sensor_routes.py:29) (51 samples, 0.86%)__init__ (<string>:4) (38 samples, 0.64%)_initialize_instance (sqlalchemy/orm/state.py:594) (28 samples, 0.47%)_declarative_constructor (sqlalchemy/orm/decl_base.py:2182) (24 samples, 0.41%)<listcomp> (app/routes/sensor_routes.py:33) (14 samples, 0.24%)collect_sensor_data (app/routes/sensor_routes.py:28) (70 samples, 1.19%)collect_sensor_data (app/routes/sensor_routes.py:45) (9 samples, 0.15%)dumps (json/__init__.py:231) (37 samples, 0.63%)encode (json/encoder.py:200) (34 samples, 0.58%)iterencode (json/encoder.py:258) (24 samples, 0.41%)acquire (asyncio/locks.py:106) (7 samples, 0.12%)__aenter__ (asyncio/locks.py:15) (21 samples, 0.36%)get_connection (redis/asyncio/connection.py:1194) (27 samples, 0.46%)ensure_connection (redis/asyncio/connection.py:1231) (6 samples, 0.10%)timeout (asyncio/timeouts.py:145) (9 samples, 0.15%)get_connection (redis/asyncio/connection.py:1198) (45 samples, 0.76%)ensure_connection (redis/asyncio/connection.py:1237) (37 samples, 0.63%)can_read_destructive (redis/asyncio/connection.py:574) (36 samples, 0.61%)can_read_destructive (redis/_parsers/base.py:433) (34 samples, 0.58%)execute_command (redis/asyncio/client.py:720) (83 samples, 1.41%)send_command (redis/asyncio/connection.py:567) (14 samples, 0.24%)_send_command_parse_response (redis/asyncio/client.py:699) (38 samples, 0.64%)send_command (redis/asyncio/connection.py:568) (24 samples, 0.41%)readuntil (asyncio/streams.py:658) (11 samples, 0.19%)_readline (redis/_parsers/base.py:468) (18 samples, 0.30%)readline (asyncio/streams.py:566) (17 samples, 0.29%)_read_response (redis/_parsers/resp2.py:90) (25 samples, 0.42%)read_response (redis/_parsers/resp2.py:82) (32 samples, 0.54%)read_response (redis/asyncio/connection.py:607) (38 samples, 0.64%)parse_response (redis/asyncio/client.py:746) (50 samples, 0.85%)execute_command (redis/asyncio/client.py:725) (113 samples, 1.91%)e..call_with_retry (redis/asyncio/retry.py:50) (110 samples, 1.86%)c.._send_command_parse_response (redis/asyncio/client.py:700) (60 samples, 1.02%)release (redis/asyncio/connection.py:1254) (10 samples, 0.17%)execute_command (redis/asyncio/client.py:735) (15 samples, 0.25%)collect_sensor_data (app/routes/sensor_routes.py:48) (270 samples, 4.57%)colle..__aenter__ (asyncio/locks.py:15) (7 samples, 0.12%)get_connection (redis/asyncio/connection.py:1194) (12 samples, 0.20%)ensure_connection (redis/asyncio/connection.py:1231) (12 samples, 0.20%)connect (redis/asyncio/connection.py:298) (9 samples, 0.15%)can_read_destructive (redis/_parsers/base.py:433) (18 samples, 0.30%)get_connection (redis/asyncio/connection.py:1198) (37 samples, 0.63%)ensure_connection (redis/asyncio/connection.py:1237) (23 samples, 0.39%)can_read_destructive (redis/asyncio/connection.py:574) (22 samples, 0.37%)execute_command (redis/asyncio/client.py:720) (57 samples, 0.97%)send_command (redis/asyncio/connection.py:567) (12 samples, 0.20%)pack_command (redis/asyncio/connection.py:655) (9 samples, 0.15%)_send_command_parse_response (redis/asyncio/client.py:699) (38 samples, 0.64%)send_command (redis/asyncio/connection.py:568) (22 samples, 0.37%)_readline (redis/_parsers/base.py:468) (15 samples, 0.25%)readline (asyncio/streams.py:566) (13 samples, 0.22%)_read_response (redis/_parsers/resp2.py:90) (21 samples, 0.36%)read_response (redis/asyncio/connection.py:607) (33 samples, 0.56%)read_response (redis/_parsers/resp2.py:82) (28 samples, 0.47%)parse_response (redis/asyncio/client.py:746) (41 samples, 0.69%)parse_response (redis/asyncio/client.py:758) (9 samples, 0.15%)__contains__ (redis/client.py:92) (6 samples, 0.10%)parse_response (redis/asyncio/client.py:761) (9 samples, 0.15%)execute_command (redis/asyncio/client.py:725) (118 samples, 2.00%)e..call_with_retry (redis/asyncio/retry.py:50) (113 samples, 1.91%)c.._send_command_parse_response (redis/asyncio/client.py:700) (69 samples, 1.17%)parse_response (redis/asyncio/client.py:762) (7 samples, 0.12%)dispatch_async (redis/event.py:115) (12 samples, 0.20%)release (redis/asyncio/connection.py:1254) (22 samples, 0.37%)execute_command (redis/asyncio/client.py:735) (25 samples, 0.42%)collect_sensor_data (app/routes/sensor_routes.py:49) (208 samples, 3.52%)col..__aenter__ (asyncio/locks.py:15) (10 samples, 0.17%)get_connection (redis/asyncio/connection.py:1194) (14 samples, 0.24%)ensure_connection (redis/asyncio/connection.py:1231) (7 samples, 0.12%)can_read_destructive (redis/_parsers/base.py:433) (17 samples, 0.29%)get_connection (redis/asyncio/connection.py:1198) (36 samples, 0.61%)ensure_connection (redis/asyncio/connection.py:1237) (24 samples, 0.41%)can_read_destructive (redis/asyncio/connection.py:574) (23 samples, 0.39%)execute_command (redis/asyncio/client.py:720) (62 samples, 1.05%)send_packed_command (redis/asyncio/connection.py:542) (6 samples, 0.10%)writelines (asyncio/streams.py:349) (6 samples, 0.10%)send_command (redis/asyncio/connection.py:567) (14 samples, 0.24%)pack_command (redis/asyncio/connection.py:648) (8 samples, 0.14%)_send_command_parse_response (redis/asyncio/client.py:699) (48 samples, 0.81%)send_command (redis/asyncio/connection.py:568) (31 samples, 0.53%)_readline (redis/_parsers/base.py:468) (23 samples, 0.39%)readline (asyncio/streams.py:566) (19 samples, 0.32%)_read_response (redis/_parsers/resp2.py:90) (26 samples, 0.44%)read_response (redis/_parsers/resp2.py:82) (31 samples, 0.53%)parse_response (redis/asyncio/client.py:746) (41 samples, 0.69%)read_response (redis/asyncio/connection.py:607) (39 samples, 0.66%)execute_command (redis/asyncio/client.py:725) (114 samples, 1.93%)e..call_with_retry (redis/asyncio/retry.py:50) (112 samples, 1.90%)c.._send_command_parse_response (redis/asyncio/client.py:700) (57 samples, 0.97%)dispatch_async (redis/event.py:115) (7 samples, 0.12%)execute_command (redis/asyncio/client.py:735) (16 samples, 0.27%)release (redis/asyncio/connection.py:1254) (11 samples, 0.19%)collect_sensor_data (app/routes/sensor_routes.py:50) (204 samples, 3.46%)col..get_connection (redis/asyncio/connection.py:1198) (20 samples, 0.34%)ensure_connection (redis/asyncio/connection.py:1237) (18 samples, 0.30%)can_read_destructive (redis/asyncio/connection.py:574) (18 samples, 0.30%)can_read_destructive (redis/_parsers/base.py:433) (14 samples, 0.24%)execute_command (redis/asyncio/client.py:720) (29 samples, 0.49%)_send_command_parse_response (redis/asyncio/client.py:699) (13 samples, 0.22%)send_command (redis/asyncio/connection.py:568) (9 samples, 0.15%)_read_response (redis/_parsers/resp2.py:90) (7 samples, 0.12%)read_response (redis/_parsers/resp2.py:82) (8 samples, 0.14%)parse_response (redis/asyncio/client.py:746) (11 samples, 0.19%)read_response (redis/asyncio/connection.py:607) (9 samples, 0.15%)execute_command (redis/asyncio/client.py:725) (37 samples, 0.63%)call_with_retry (redis/asyncio/retry.py:50) (34 samples, 0.58%)_send_command_parse_response (redis/asyncio/client.py:700) (18 samples, 0.30%)collect_sensor_data (app/routes/sensor_routes.py:54) (88 samples, 1.49%)wrapper (redis/utils.py:417) (8 samples, 0.14%)get_connection (redis/asyncio/connection.py:1194) (6 samples, 0.10%)get_connection (redis/asyncio/connection.py:1198) (7 samples, 0.12%)ensure_connection (redis/asyncio/connection.py:1237) (6 samples, 0.10%)can_read_destructive (redis/asyncio/connection.py:574) (6 samples, 0.10%)execute_command (redis/asyncio/client.py:720) (14 samples, 0.24%)_send_command_parse_response (redis/asyncio/client.py:699) (14 samples, 0.24%)send_command (redis/asyncio/connection.py:568) (9 samples, 0.15%)read_response (redis/asyncio/connection.py:607) (6 samples, 0.10%)read_response (redis/_parsers/resp2.py:82) (6 samples, 0.10%)_read_response (redis/_parsers/resp2.py:90) (6 samples, 0.10%)parse_response (redis/asyncio/client.py:746) (7 samples, 0.12%)execute_command (redis/asyncio/client.py:725) (32 samples, 0.54%)call_with_retry (redis/asyncio/retry.py:50) (28 samples, 0.47%)_send_command_parse_response (redis/asyncio/client.py:700) (13 samples, 0.22%)collect_sensor_data (app/routes/sensor_routes.py:57) (65 samples, 1.10%)wrapper (redis/utils.py:417) (8 samples, 0.14%)app (fastapi/routing.py:430) (1,015 samples, 17.19%)app (fastapi/routing.py:430)run_endpoint_function (fastapi/routing.py:316) (1,005 samples, 17.02%)run_endpoint_function (fas..serialize_response (fastapi/routing.py:267) (17 samples, 0.29%)validate (fastapi/_compat/v2.py:178) (17 samples, 0.29%)validate_python (pydantic/type_adapter.py:441) (16 samples, 0.27%)serialize_response (fastapi/routing.py:285) (18 samples, 0.30%)serialize (fastapi/_compat/v2.py:200) (18 samples, 0.30%)dump_python (pydantic/type_adapter.py:605) (18 samples, 0.30%)jsonable_encoder (fastapi/encoders.py:275) (6 samples, 0.10%)jsonable_encoder (fastapi/encoders.py:294) (9 samples, 0.15%)app (fastapi/routing.py:452) (64 samples, 1.08%)serialize_response (fastapi/routing.py:305) (27 samples, 0.46%)__init__ (starlette/responses.py:46) (37 samples, 0.63%)render (starlette/responses.py:192) (34 samples, 0.58%)dumps (json/__init__.py:238) (28 samples, 0.47%)encode (json/encoder.py:200) (28 samples, 0.47%)iterencode (json/encoder.py:258) (25 samples, 0.42%)app (fastapi/routing.py:464) (45 samples, 0.76%)__init__ (starlette/responses.py:189) (45 samples, 0.76%)__init__ (starlette/responses.py:47) (6 samples, 0.10%)app (fastapi/routing.py:106) (2,446 samples, 41.43%)app (fastapi/routing.py:106)app (fastapi/routing.py:467) (6 samples, 0.10%)__call__ (starlette/responses.py:157) (13 samples, 0.22%)sender (starlette/_exception_handler.py:39) (9 samples, 0.15%)sender (starlette/_exception_handler.py:39) (9 samples, 0.15%)send (anyio/streams/memory.py:247) (13 samples, 0.22%)checkpoint (anyio/lowlevel.py:39) (12 samples, 0.20%)send_nowait (anyio/streams/memory.py:224) (9 samples, 0.15%)send_with_compression (starlette/middleware/gzip.py:68) (27 samples, 0.46%)send_no_error (starlette/middleware/base.py:134) (26 samples, 0.44%)send (anyio/streams/memory.py:249) (12 samples, 0.20%)create_event (anyio/_backends/_asyncio.py:2446) (7 samples, 0.12%)send (anyio/streams/memory.py:252) (13 samples, 0.22%)__new__ (anyio/_core/_synchronization.py:85) (9 samples, 0.15%)wait (asyncio/locks.py:210) (7 samples, 0.12%)send (anyio/streams/memory.py:255) (11 samples, 0.19%)wait (anyio/_backends/_asyncio.py:1812) (11 samples, 0.19%)send_with_compression (starlette/middleware/gzip.py:69) (36 samples, 0.61%)send_no_error (starlette/middleware/base.py:134) (34 samples, 0.58%)apply_compression (starlette/middleware/gzip.py:133) (6 samples, 0.10%)write (gzip.py:289) (6 samples, 0.10%)send_with_compression (starlette/middleware/gzip.py:72) (10 samples, 0.17%)app (fastapi/routing.py:107) (102 samples, 1.73%)__call__ (starlette/responses.py:164) (87 samples, 1.47%)sender (starlette/_exception_handler.py:39) (83 samples, 1.41%)sender (starlette/_exception_handler.py:39) (82 samples, 1.39%)app (fastapi/routing.py:120) (2,647 samples, 44.83%)app (fastapi/routing.py:120)wrapped_app (starlette/_exception_handler.py:42) (2,620 samples, 44.38%)wrapped_app (starlette/_exception_handler.py:42)coro (starlette/middleware/base.py:144) (2,910 samples, 49.29%)coro (starlette/middleware/base.py:144)__call__ (starlette/middleware/gzip.py:29) (2,834 samples, 48.00%)__call__ (starlette/middleware/gzip.py:29)__call__ (starlette/middleware/gzip.py:130) (2,787 samples, 47.21%)__call__ (starlette/middleware/gzip.py:130)__call__ (starlette/middleware/gzip.py:46) (2,780 samples, 47.09%)__call__ (starlette/middleware/gzip.py:46)__call__ (starlette/middleware/exceptions.py:63) (2,758 samples, 46.71%)__call__ (starlette/middleware/exceptions.py:63)wrapped_app (starlette/_exception_handler.py:42) (2,743 samples, 46.46%)wrapped_app (starlette/_exception_handler.py:42)__call__ (fastapi/middleware/asyncexitstack.py:18) (2,714 samples, 45.97%)__call__ (fastapi/middleware/asyncexitstack.py:18)__call__ (starlette/routing.py:716) (2,702 samples, 45.77%)__call__ (starlette/routing.py:716)app (starlette/routing.py:736) (2,676 samples, 45.33%)app (starlette/routing.py:736)handle (starlette/routing.py:290) (2,665 samples, 45.14%)handle (starlette/routing.py:290)data_received (asyncio/streams.py:285) (11 samples, 0.19%)feed_data (asyncio/streams.py:504) (19 samples, 0.32%)_wakeup_waiter (asyncio/streams.py:478) (10 samples, 0.17%)data_received (asyncio/streams.py:287) (31 samples, 0.53%)data_received (uvicorn/protocols/http/httptools_impl.py:168) (6 samples, 0.10%)on_headers_complete (uvicorn/protocols/http/httptools_impl.py:253) (8 samples, 0.14%)on_headers_complete (uvicorn/protocols/http/httptools_impl.py:290) (6 samples, 0.10%)data_received (uvicorn/protocols/http/httptools_impl.py:171) (65 samples, 1.10%)digest (hmac.py:159) (21 samples, 0.36%)_init_hmac (hmac.py:67) (6 samples, 0.10%)new (hmac.py:184) (11 samples, 0.19%)__init__ (hmac.py:60) (9 samples, 0.15%)release (asyncpg/pool.py:239) (14 samples, 0.24%)reset (asyncpg/connection.py:1567) (14 samples, 0.24%)timeout (asyncio/timeouts.py:146) (14 samples, 0.24%)__init__ (asyncio/timeouts.py:40) (14 samples, 0.24%)__contains__ (uvicorn/middleware/proxy_headers.py:109) (7 samples, 0.12%)__contains__ (uvicorn/middleware/proxy_headers.py:117) (16 samples, 0.27%)ip_address (ipaddress.py:45) (15 samples, 0.25%)__init__ (ipaddress.py:1319) (13 samples, 0.22%)_ip_int_from_string (ipaddress.py:1209) (10 samples, 0.17%)__call__ (uvicorn/middleware/proxy_headers.py:34) (30 samples, 0.51%)__call__ (starlette/middleware/cors.py:81) (15 samples, 0.25%)__call__ (starlette/middleware/cors.py:82) (15 samples, 0.25%)get (<frozen _collections_abc>:774) (14 samples, 0.24%)__call__ (starlette/middleware/base.py:101) (8 samples, 0.14%)__call__ (starlette/middleware/base.py:106) (7 samples, 0.12%)__init__ (starlette/middleware/base.py:29) (6 samples, 0.10%)create_event (anyio/_backends/_asyncio.py:2446) (6 samples, 0.10%)__call__ (starlette/middleware/base.py:108) (18 samples, 0.30%)__new__ (anyio/_core/_synchronization.py:85) (11 samples, 0.19%)__init__ (<string>:4) (7 samples, 0.12%)__call__ (typing.py:1289) (9 samples, 0.15%)__new__ (anyio/_core/_streams.py:51) (15 samples, 0.25%)__call__ (starlette/middleware/base.py:189) (27 samples, 0.46%)__new__ (anyio/_core/_streams.py:52) (8 samples, 0.14%)__call__ (starlette/middleware/base.py:191) (12 samples, 0.20%)__enter__ (anyio/_backends/_asyncio.py:428) (9 samples, 0.15%)__aenter__ (anyio/_backends/_asyncio.py:731) (16 samples, 0.27%)__enter__ (anyio/_backends/_asyncio.py:425) (6 samples, 0.10%)current_task (asyncio/tasks.py:38) (6 samples, 0.10%)__aexit__ (anyio/_backends/_asyncio.py:750) (27 samples, 0.46%)__init__ (anyio/_backends/_asyncio.py:724) (6 samples, 0.10%)create_task_group (anyio/_backends/_asyncio.py:2442) (8 samples, 0.14%)__call__ (starlette/middleware/base.py:192) (64 samples, 1.08%)create_task_group (anyio/_core/_tasks.py:163) (10 samples, 0.17%)dispatch (app/middleware.py:13) (16 samples, 0.27%)uuid4 (uuid.py:723) (10 samples, 0.17%)_spawn (anyio/_backends/_asyncio.py:874) (14 samples, 0.24%)get_callable_name (anyio/_backends/_asyncio.py:348) (12 samples, 0.20%)create_task (asyncio/tasks.py:384) (14 samples, 0.24%)call_next (starlette/middleware/base.py:148) (41 samples, 0.69%)start_soon (anyio/_backends/_asyncio.py:904) (41 samples, 0.69%)_spawn (anyio/_backends/_asyncio.py:884) (18 samples, 0.30%)receive (anyio/streams/memory.py:115) (29 samples, 0.49%)checkpoint (anyio/lowlevel.py:39) (28 samples, 0.47%)receive (anyio/streams/memory.py:117) (6 samples, 0.10%)__init__ (anyio/_backends/_asyncio.py:1800) (17 samples, 0.29%)__init__ (asyncio/locks.py:168) (13 samples, 0.22%)create_event (anyio/_backends/_asyncio.py:2446) (19 samples, 0.32%)receive (anyio/streams/memory.py:120) (26 samples, 0.44%)__new__ (anyio/_core/_synchronization.py:85) (21 samples, 0.36%)__call__ (typing.py:1289) (9 samples, 0.15%)__init__ (<string>:3) (7 samples, 0.12%)get_current_task (anyio/_core/_testing.py:63) (7 samples, 0.12%)receive (anyio/streams/memory.py:121) (14 samples, 0.24%)receive (anyio/streams/memory.py:125) (22 samples, 0.37%)wait (anyio/_backends/_asyncio.py:1812) (22 samples, 0.37%)wait (asyncio/locks.py:210) (19 samples, 0.32%)_get_loop (asyncio/mixins.py:16) (16 samples, 0.27%)call_next (starlette/middleware/base.py:151) (109 samples, 1.85%)c..dispatch (app/middleware.py:15) (164 samples, 2.78%)di..dispatch (app/middleware.py:19) (8 samples, 0.14%)_log (logging/__init__.py:1622) (10 samples, 0.17%)makeRecord (logging/__init__.py:1601) (19 samples, 0.32%)_log (logging/__init__.py:1632) (21 samples, 0.36%)handle (logging/__init__.py:974) (9 samples, 0.15%)formatTime (logging/__init__.py:624) (6 samples, 0.10%)formatTime (logging/__init__.py:628) (6 samples, 0.10%)format (logging/__init__.py:689) (13 samples, 0.22%)format (logging/__init__.py:690) (6 samples, 0.10%)emit (logging/__init__.py:1110) (24 samples, 0.41%)format (logging/__init__.py:953) (23 samples, 0.39%)emit (logging/__init__.py:1113) (48 samples, 0.81%)dispatch (app/middleware.py:21) (126 samples, 2.13%)d..info (logging/__init__.py:1489) (124 samples, 2.10%)i.._log (logging/__init__.py:1634) (91 samples, 1.54%)handle (logging/__init__.py:1644) (90 samples, 1.52%)callHandlers (logging/__init__.py:1706) (86 samples, 1.46%)handle (logging/__init__.py:978) (75 samples, 1.27%)path (starlette/datastructures.py:87) (6 samples, 0.10%)dispatch (app/middleware.py:24) (15 samples, 0.25%)url (starlette/requests.py:103) (8 samples, 0.14%)__call__ (starlette/middleware/base.py:193) (346 samples, 5.86%)__call_.._log (logging/__init__.py:1632) (8 samples, 0.14%)makeRecord (logging/__init__.py:1601) (8 samples, 0.14%)formatMessage (uvicorn/logging.py:106) (6 samples, 0.10%)formatMessage (uvicorn/logging.py:117) (10 samples, 0.17%)formatMessage (uvicorn/logging.py:65) (7 samples, 0.12%)copy (copy.py:102) (8 samples, 0.14%)copy (copy.py:82) (6 samples, 0.10%)format (logging/__init__.py:690) (48 samples, 0.81%)formatMessage (uvicorn/logging.py:98) (28 samples, 0.47%)copy (copy.py:92) (10 samples, 0.17%)emit (logging/__init__.py:1110) (57 samples, 0.97%)format (logging/__init__.py:953) (56 samples, 0.95%)send (uvicorn/protocols/http/httptools_impl.py:476) (98 samples, 1.66%)info (logging/__init__.py:1489) (96 samples, 1.63%)_log (logging/__init__.py:1634) (83 samples, 1.41%)handle (logging/__init__.py:1644) (83 samples, 1.41%)callHandlers (logging/__init__.py:1706) (82 samples, 1.39%)handle (logging/__init__.py:978) (80 samples, 1.36%)emit (logging/__init__.py:1114) (22 samples, 0.37%)flush (logging/__init__.py:1094) (22 samples, 0.37%)send (uvicorn/protocols/http/httptools_impl.py:480) (10 samples, 0.17%)get_path_with_query_string (uvicorn/protocols/utils.py:53) (9 samples, 0.15%)__call__ (starlette/middleware/base.py:223) (131 samples, 2.22%)_.._send (starlette/middleware/errors.py:161) (129 samples, 2.18%)_..receive (anyio/streams/memory.py:115) (9 samples, 0.15%)checkpoint (anyio/lowlevel.py:39) (9 samples, 0.15%)body_stream (starlette/middleware/base.py:174) (16 samples, 0.27%)__anext__ (anyio/abc/_streams.py:41) (16 samples, 0.27%)receive (anyio/streams/memory.py:117) (6 samples, 0.10%)__call__ (starlette/middleware/base.py:232) (28 samples, 0.47%)__call__ (starlette/middleware/base.py:238) (9 samples, 0.15%)_send (starlette/middleware/errors.py:161) (8 samples, 0.14%)__call__ (starlette/middleware/base.py:194) (180 samples, 3.05%)__c..__call__ (starlette/middleware/base.py:241) (11 samples, 0.19%)_send (starlette/middleware/errors.py:161) (11 samples, 0.19%)send (uvicorn/protocols/http/httptools_impl.py:549) (10 samples, 0.17%)on_response_complete (uvicorn/protocols/http/httptools_impl.py:332) (9 samples, 0.15%)run_asgi (uvicorn/protocols/http/httptools_impl.py:409) (769 samples, 13.03%)run_asgi (uvicorn/pr..__call__ (uvicorn/middleware/proxy_headers.py:60) (734 samples, 12.43%)__call__ (uvicorn/m..__call__ (fastapi/applications.py:1139) (732 samples, 12.40%)__call__ (fastapi/a..__call__ (starlette/applications.py:107) (729 samples, 12.35%)__call__ (starlett..__call__ (starlette/middleware/errors.py:164) (723 samples, 12.25%)__call__ (starlett..__call__ (starlette/middleware/cors.py:85) (684 samples, 11.59%)__call__ (starlet..run_asgi (uvicorn/protocols/http/httptools_impl.py:414) (6 samples, 0.10%)error (logging/__init__.py:1518) (6 samples, 0.10%)<module> (uvicorn:8) (5,075 samples, 85.96%)<module> (uvicorn:8)__call__ (click/core.py:1485) (5,075 samples, 85.96%)__call__ (click/core.py:1485)main (click/core.py:1406) (5,075 samples, 85.96%)main (click/core.py:1406)invoke (click/core.py:1269) (5,075 samples, 85.96%)invoke (click/core.py:1269)invoke (click/core.py:824) (5,075 samples, 85.96%)invoke (click/core.py:824)main (uvicorn/main.py:471) (5,075 samples, 85.96%)main (uvicorn/main.py:471)run (uvicorn/main.py:593) (5,075 samples, 85.96%)run (uvicorn/main.py:593)run (uvicorn/server.py:67) (5,075 samples, 85.96%)run (uvicorn/server.py:67)asyncio_run (uvicorn/_compat.py:30) (5,075 samples, 85.96%)asyncio_run (uvicorn/_compat.py:30)run (asyncio/runners.py:118) (5,075 samples, 85.96%)run (asyncio/runners.py:118)wrap (starlette/middleware/base.py:120) (8 samples, 0.14%)wait (anyio/_backends/_asyncio.py:1812) (8 samples, 0.14%)expunge_all (sqlalchemy/orm/session.py:2588) (7 samples, 0.12%)_detach_states (sqlalchemy/orm/state.py:474) (12 samples, 0.20%)_close_impl (sqlalchemy/orm/session.py:2573) (33 samples, 0.56%)expunge_all (sqlalchemy/orm/session.py:2592) (17 samples, 0.29%)close (sqlalchemy/orm/session.py:1417) (24 samples, 0.41%)close (sqlalchemy/engine/base.py:2597) (24 samples, 0.41%)_do_close (sqlalchemy/engine/base.py:2735) (23 samples, 0.39%)_close_impl (sqlalchemy/engine/base.py:2721) (23 samples, 0.39%)_connection_rollback_impl (sqlalchemy/engine/base.py:2713) (23 samples, 0.39%)_rollback_impl (sqlalchemy/engine/base.py:1128) (14 samples, 0.24%)do_rollback (sqlalchemy/engine/default.py:712) (11 samples, 0.19%)_finalize_fairy (sqlalchemy/pool/base.py:1034) (16 samples, 0.27%)checkin (sqlalchemy/pool/base.py:771) (13 samples, 0.22%)_return_conn (sqlalchemy/pool/base.py:456) (13 samples, 0.22%)_do_return_conn (sqlalchemy/pool/impl.py:144) (12 samples, 0.20%)put (sqlalchemy/util/queue.py:289) (12 samples, 0.20%)put_nowait (sqlalchemy/util/queue.py:281) (11 samples, 0.19%)_checkin (sqlalchemy/pool/base.py:1386) (30 samples, 0.51%)close (sqlalchemy/engine/base.py:1258) (32 samples, 0.54%)close (sqlalchemy/pool/base.py:1511) (31 samples, 0.53%)close (sqlalchemy/orm/session.py:1419) (34 samples, 0.58%)close (sqlalchemy/orm/session.py:2507) (128 samples, 2.17%)c.._close_impl (sqlalchemy/orm/session.py:2576) (77 samples, 1.30%)close (<string>:2) (77 samples, 1.30%)_go (sqlalchemy/orm/state_changes.py:137) (75 samples, 1.27%)close (sqlalchemy/orm/session.py:1432) (8 samples, 0.14%)expect (sqlalchemy/sql/coercions.py:416) (6 samples, 0.10%)_execute_internal (sqlalchemy/orm/session.py:2147) (10 samples, 0.17%)__get__ (sqlalchemy/event/base.py:451) (16 samples, 0.27%)_for_instance (sqlalchemy/event/base.py:185) (9 samples, 0.15%)_for_class (sqlalchemy/event/base.py:181) (7 samples, 0.12%)_execute_internal (sqlalchemy/orm/session.py:2174) (25 samples, 0.42%)orm_pre_session_exec (sqlalchemy/orm/context.py:506) (7 samples, 0.12%)orm_pre_session_exec (sqlalchemy/orm/context.py:507) (6 samples, 0.10%)_execute_internal (sqlalchemy/orm/session.py:2228) (28 samples, 0.47%)__init__ (weakref.py:369) (7 samples, 0.12%)_take_snapshot (sqlalchemy/orm/session.py:1089) (9 samples, 0.15%)__init__ (sqlalchemy/orm/session.py:958) (32 samples, 0.54%)_take_snapshot (sqlalchemy/orm/session.py:1092) (11 samples, 0.19%)_connection_for_bind (sqlalchemy/orm/session.py:2107) (45 samples, 0.76%)_autobegin_t (sqlalchemy/orm/session.py:1879) (44 samples, 0.75%)get (sqlalchemy/util/queue.py:311) (12 samples, 0.20%)await_only (sqlalchemy/util/_concurrency_py3k.py:132) (7 samples, 0.12%)_do_get (sqlalchemy/pool/impl.py:156) (21 samples, 0.36%)get (sqlalchemy/util/queue.py:312) (7 samples, 0.12%)checkout (sqlalchemy/pool/base.py:711) (27 samples, 0.46%)_checkout (sqlalchemy/pool/base.py:1264) (40 samples, 0.68%)_checkout (sqlalchemy/pool/base.py:1301) (9 samples, 0.15%)_do_ping_w_event (sqlalchemy/engine/default.py:729) (7 samples, 0.12%)do_ping (sqlalchemy/dialects/postgresql/asyncpg.py:1160) (6 samples, 0.10%)ping (sqlalchemy/dialects/postgresql/asyncpg.py:816) (6 samples, 0.10%)__init__ (sqlalchemy/engine/base.py:143) (63 samples, 1.07%)raw_connection (sqlalchemy/engine/base.py:3309) (62 samples, 1.05%)connect (sqlalchemy/pool/base.py:447) (58 samples, 0.98%)_checkout (sqlalchemy/pool/base.py:1318) (7 samples, 0.12%)__get__ (sqlalchemy/event/base.py:451) (6 samples, 0.10%)_connection_for_bind (sqlalchemy/orm/session.py:1187) (84 samples, 1.42%)connect (sqlalchemy/engine/base.py:3285) (84 samples, 1.42%)__init__ (sqlalchemy/engine/base.py:164) (12 samples, 0.20%)_connection_for_bind (sqlalchemy/orm/session.py:1256) (6 samples, 0.10%)_go (sqlalchemy/orm/state_changes.py:137) (106 samples, 1.80%)_.._execute_internal (sqlalchemy/orm/session.py:2239) (156 samples, 2.64%)_e.._connection_for_bind (sqlalchemy/orm/session.py:2108) (109 samples, 1.85%)_.._connection_for_bind (<string>:2) (109 samples, 1.85%)_.._execute_on_connection (sqlalchemy/sql/elements.py:524) (8 samples, 0.14%)_gen_cache_key (sqlalchemy/sql/cache_key.py:246) (6 samples, 0.10%)_generated_cache_key_traversal (<string>:24) (23 samples, 0.39%)_generated_cache_key_traversal (<string>:31) (7 samples, 0.12%)__get__ (sqlalchemy/util/langhelpers.py:1226) (6 samples, 0.10%)_gen_cache_key (sqlalchemy/sql/cache_key.py:272) (45 samples, 0.76%)_gen_cache_key (sqlalchemy/sql/cache_key.py:272) (8 samples, 0.14%)_gen_cache_key (sqlalchemy/sql/cache_key.py:244) (11 samples, 0.19%)_gen_cache_key (sqlalchemy/sql/cache_key.py:293) (34 samples, 0.58%)_gen_cache_key (sqlalchemy/sql/cache_key.py:335) (53 samples, 0.90%)<listcomp> (sqlalchemy/sql/cache_key.py:336) (52 samples, 0.88%)_generate_cache_key (sqlalchemy/sql/cache_key.py:382) (116 samples, 1.96%)_..oneshot (sqlalchemy/util/langhelpers.py:1351) (126 samples, 2.13%)o.._generate_cache_key (sqlalchemy/sql/cache_key.py:413) (123 samples, 2.08%)_.._compile_w_cache (sqlalchemy/sql/elements.py:695) (138 samples, 2.34%)_..get (sqlalchemy/util/_collections.py:531) (10 samples, 0.17%)_execute_clauseelement (sqlalchemy/engine/base.py:1633) (154 samples, 2.61%)_e.._compile_w_cache (sqlalchemy/sql/elements.py:712) (12 samples, 0.20%)_execute_clauseelement (sqlalchemy/engine/base.py:1639) (6 samples, 0.10%)_init_compiled (sqlalchemy/engine/default.py:1416) (6 samples, 0.10%)<listcomp> (sqlalchemy/engine/default.py:1417) (6 samples, 0.10%)_init_compiled (sqlalchemy/engine/default.py:1438) (8 samples, 0.14%)_execute_context (sqlalchemy/engine/base.py:1815) (27 samples, 0.46%)execute (sqlalchemy/dialects/postgresql/asyncpg.py:585) (17 samples, 0.29%)await_only (sqlalchemy/util/_concurrency_py3k.py:132) (14 samples, 0.24%)_exec_single_context (sqlalchemy/engine/base.py:1967) (19 samples, 0.32%)do_execute (sqlalchemy/engine/default.py:952) (18 samples, 0.30%)_setup_result_proxy (sqlalchemy/engine/default.py:1875) (14 samples, 0.24%)orm_execute_statement (sqlalchemy/orm/context.py:306) (261 samples, 4.42%)orm_e..execute (sqlalchemy/engine/base.py:1419) (258 samples, 4.37%)execu.._execute_on_connection (sqlalchemy/sql/elements.py:527) (250 samples, 4.23%)_exec.._execute_clauseelement (sqlalchemy/engine/base.py:1641) (81 samples, 1.37%)_execute_context (sqlalchemy/engine/base.py:1846) (49 samples, 0.83%)_exec_single_context (sqlalchemy/engine/base.py:1983) (23 samples, 0.39%)orm_setup_cursor_result (sqlalchemy/orm/context.py:606) (8 samples, 0.14%)_instance_processor (sqlalchemy/orm/loading.py:812) (24 samples, 0.41%)_instance_processor (sqlalchemy/orm/loading.py:975) (7 samples, 0.12%)row_processor (sqlalchemy/orm/context.py:2786) (47 samples, 0.80%)instances (sqlalchemy/orm/loading.py:114) (52 samples, 0.88%)<listcomp> (sqlalchemy/orm/loading.py:115) (51 samples, 0.86%)__init__ (sqlalchemy/engine/result.py:281) (8 samples, 0.14%)instances (sqlalchemy/orm/loading.py:204) (17 samples, 0.29%)_raw_all_rows (sqlalchemy/engine/result.py:540) (7 samples, 0.12%)_fetchall_impl (sqlalchemy/engine/cursor.py:2252) (6 samples, 0.10%)chunks (sqlalchemy/orm/loading.py:220) (25 samples, 0.42%)_raw_all_rows (sqlalchemy/engine/result.py:541) (18 samples, 0.30%)<listcomp> (sqlalchemy/engine/result.py:541) (17 samples, 0.29%)__init__ (sqlalchemy/orm/state.py:204) (31 samples, 0.53%)new_instance (sqlalchemy/orm/instrumentation.py:509) (50 samples, 0.85%)__init__ (sqlalchemy/orm/state.py:206) (10 samples, 0.17%)_instance (sqlalchemy/orm/loading.py:1115) (54 samples, 0.91%)_instance (sqlalchemy/orm/loading.py:1117) (6 samples, 0.10%)_instance (sqlalchemy/orm/loading.py:1146) (7 samples, 0.12%)instances (sqlalchemy/orm/loading.py:262) (103 samples, 1.74%)chunks (sqlalchemy/orm/loading.py:224) (74 samples, 1.25%)<listcomp> (sqlalchemy/orm/loading.py:224) (74 samples, 1.25%)all (5,904 samples, 100%)execute (sqlalchemy/orm/session.py:2351) (697 samples, 11.81%)execute (sqlalchem.._execute_internal (sqlalchemy/orm/session.py:2249) (466 samples, 7.89%)_execute_in..orm_execute_statement (sqlalchemy/orm/context.py:309) (204 samples, 3.46%)orm..orm_setup_cursor_result (sqlalchemy/orm/context.py:616) (188 samples, 3.18%)orm.. \ No newline at end of file diff --git a/scripts/mock_data.py b/scripts/mock_data.py index 8723974..b94ef9f 100644 --- a/scripts/mock_data.py +++ b/scripts/mock_data.py @@ -4,9 +4,12 @@ import time BASE_URL = "http://localhost:8000" +API_KEY = "" # 발급받은 API Key 입력 (POST /api/robots, /api/sensors 필요) TOTAL_ROBOT = 10 TOTAL_REQUESTS = 1000 +HEADERS = {"X-API-Key": API_KEY} if API_KEY else {} + def create_robot() : """로봇 10대 생성""" print("로봇 생성") @@ -21,7 +24,8 @@ def create_robot() : response = requests.post( f"{BASE_URL}/api/robots", - json = robot_data + json = robot_data, + headers = HEADERS, ) if response.status_code == 201 : @@ -96,6 +100,7 @@ def create_sensor_data() : response = requests.post( f"{BASE_URL}/api/sensors", json=payload, + headers=HEADERS, timeout=5 ) diff --git a/test/conftest.py b/test/conftest.py index f5e4714..faee725 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -156,9 +156,22 @@ async def override_get_db(): app.dependency_overrides[get_db] = override_get_db # get_redis는 직접 호출이므로 모듈 변수 교체 - fake_redis = FakeRedis() + # TEST_REDIS_HOST 있으면 실제 Redis(CI), 없으면 FakeRedis(로컬 기본) + test_redis_host = os.getenv("TEST_REDIS_HOST") + if test_redis_host: + from redis.asyncio import Redis + test_redis = Redis( + host=test_redis_host, + port=int(os.getenv("TEST_REDIS_PORT", "6379")), + db=0, + decode_responses=True, + encoding="utf-8", + ) + await test_redis.flushdb() + else: + test_redis = FakeRedis() original_redis = redis_module.redis_client - redis_module.redis_client = fake_redis + redis_module.redis_client = test_redis # asyncpg_pool은 health_check에서 사용 — mock 주입 mock_pool = MagicMock() @@ -183,5 +196,8 @@ async def override_get_db(): # 정리 app.dependency_overrides.clear() + if test_redis_host: + await test_redis.flushdb() + await test_redis.aclose() redis_module.redis_client = original_redis db_module.asyncpg_pool = original_pool diff --git a/test/locustfile.py b/test/locustfile.py index 2d1a487..951608f 100644 --- a/test/locustfile.py +++ b/test/locustfile.py @@ -1,8 +1,11 @@ from locust import HttpUser, task, constant import random import time +import os from datetime import datetime +API_KEY = os.getenv("LOCUST_API_KEY", "8_pE3-Gxz788YhXeX7lZUKt9W4AH9wVe6LZFCuq-f10") + def generate_sensor_payload(robot_id, timestamp) : """페이로드 생성""" @@ -56,6 +59,7 @@ class User(HttpUser) : def on_start(self) : """시작 시 로봇 ID 할당""" + self.client.headers.update({"X-API-Key": API_KEY}) self.robot_id = random.randint(1, 10) @task(100) diff --git a/test/puttest.py b/test/puttest.py deleted file mode 100644 index c64416d..0000000 --- a/test/puttest.py +++ /dev/null @@ -1,20 +0,0 @@ -# test_put.py 만들어서 실행 -import requests - -# 1. 로봇 1번 현재 상태 확인 -response = requests.get("http://localhost/api/robots/1") -print("변경 전:", response.json()) - -# 2. PUT으로 상태 변경 -response = requests.put( - "http://localhost/api/robots/1", - json={ - "status": "maintenance", - "battery_level": 50 - } -) -print("PUT 응답:", response.status_code, response.text) - -# 3. 다시 확인 -response = requests.get("http://localhost/api/robots/1") -print("변경 후:", response.json()) \ No newline at end of file diff --git a/test/test_single_request.py b/test/test_single_request.py deleted file mode 100644 index e318efd..0000000 --- a/test/test_single_request.py +++ /dev/null @@ -1,48 +0,0 @@ -# 성능 확인용 - -import requests -import time - -url = "http://localhost:8000/api/sensors" - -data = { - "robot_id": 1, - "timestamp": "2024-01-04T10:30:00", - "sensors": [ - { - "sensor_type": "IMU", - "data": { - "acceleration": {"x": 1.2, "y": -0.5, "z": 9.8}, - "gyroscope": {"x": 0.1, "y": 0.2, "z": 0.3} - } - }, - { - "sensor_type": "GPS", - "data": { - "latitude": 37.5665, - "longitude": 126.9780 - } - }, - { - "sensor_type": "LiDAR", - "data": { - "distance": 3.5, - "angle": 45 - } - } - ] -} - -print("10번 연속 요청 시작...\n") - -times = [] -for i in range(10): - start = time.time() - response = requests.post(url, json=data) - elapsed = (time.time() - start) * 1000 - times.append(elapsed) - print(f"{i+1}번째 요청: {elapsed:.2f}ms (응답: {response.status_code})") - -print(f"\n평균: {sum(times) / len(times):.2f}ms") -print(f"최소: {min(times):.2f}ms") -print(f"최대: {max(times):.2f}ms") \ No newline at end of file