OPCIÓN A: A2 Virtual Scroll + A3 Celery + A4 asyncpg PoC + A5 particionamiento
A2 — Virtual scroll en tablas grandes: - Nuevo helper VirtualScroll en pos/static/js/virtual-scroll.js - inventory.js: tabla de productos con virtual scroll - customers.js: tabla de clientes con virtual scroll - fleet.js: renderMaintenance() y renderHistory() con virtual scroll - Templates envueltos en .vs-container para scroll A3 — Celery worker queue: - pos/celery_app.py + pos/tasks.py (warm cache, bulk import, reports) - Blueprint tasks_bp.py con endpoints /pos/api/tasks/* - Script scripts/start_celery.sh A4 — asyncpg + Quart PoC: - pos/async_catalog.py: endpoint /pos/api/catalog/async-search - scripts/benchmark_async_catalog.py: benchmark Flask vs Quart A5 — Particionar vehicle_parts: - scripts/partition_vehicle_parts.py: migración segura por hash (16 particiones) - Soporta --dry-run, --skip-swap, --skip-drop Tests: 36/36 pasando
This commit is contained in:
93
pos/async_catalog.py
Normal file
93
pos/async_catalog.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Async catalog search PoC using Quart + asyncpg.
|
||||
|
||||
Run:
|
||||
hypercorn async_catalog:app --bind 0.0.0.0:5002
|
||||
|
||||
Endpoint:
|
||||
GET /pos/api/catalog/async-search?q=filtro&limit=50&tenant_id=1
|
||||
|
||||
This demonstrates I/O non-blocking search using asyncpg.
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import asyncpg
|
||||
from quart import Quart, request, jsonify, g
|
||||
|
||||
app = Quart(__name__)
|
||||
|
||||
MASTER_DB_URL = os.environ.get('MASTER_DB_URL', 'postgresql://postgres@/nexus_autoparts')
|
||||
|
||||
# Shared connection pool
|
||||
_pool = None
|
||||
|
||||
async def _get_pool():
|
||||
global _pool
|
||||
if _pool is None:
|
||||
_pool = await asyncpg.create_pool(MASTER_DB_URL, min_size=2, max_size=10)
|
||||
return _pool
|
||||
|
||||
|
||||
@app.before_serving
|
||||
async def startup():
|
||||
await _get_pool()
|
||||
|
||||
|
||||
@app.after_serving
|
||||
async def shutdown():
|
||||
global _pool
|
||||
if _pool:
|
||||
await _pool.close()
|
||||
_pool = None
|
||||
|
||||
|
||||
@app.route('/pos/api/catalog/async-search')
|
||||
async def async_search():
|
||||
q = request.args.get('q', '').strip()
|
||||
if not q or len(q) < 2:
|
||||
return jsonify({'data': []})
|
||||
limit = min(request.args.get('limit', 50, type=int), 100)
|
||||
|
||||
pool = await _get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
# Simple text search (PoC scope)
|
||||
clean_q = q.upper().replace(' ', '')
|
||||
rows = await conn.fetch("""
|
||||
SELECT p.id_part, p.oem_part_number, p.name_part, p.name_es,
|
||||
p.image_url, p.group_id
|
||||
FROM parts p
|
||||
WHERE REPLACE(UPPER(p.oem_part_number), ' ', '') LIKE $1
|
||||
OR p.name_part ILIKE $2
|
||||
OR p.name_es ILIKE $2
|
||||
ORDER BY p.oem_part_number
|
||||
LIMIT $3
|
||||
""", f'%{clean_q}%', f'%{q}%', limit)
|
||||
|
||||
part_ids = [r['id_part'] for r in rows]
|
||||
vehicle_map = {}
|
||||
if part_ids:
|
||||
vrows = await conn.fetch("""
|
||||
SELECT part_id, name_brand, name_model, year_car
|
||||
FROM part_vehicle_preview
|
||||
WHERE part_id = ANY($1)
|
||||
""", part_ids)
|
||||
for vr in vrows:
|
||||
vehicle_map[vr['part_id']] = f"{vr['name_brand']} {vr['name_model']} {vr['year_car']}"
|
||||
|
||||
results = []
|
||||
for r in rows:
|
||||
results.append({
|
||||
'id': r['id_part'],
|
||||
'oem_part_number': r['oem_part_number'],
|
||||
'name': r['name_part'],
|
||||
'name_es': r['name_es'],
|
||||
'image_url': r['image_url'],
|
||||
'group_id': r['group_id'],
|
||||
'vehicle_info': vehicle_map.get(r['id_part'], ''),
|
||||
})
|
||||
|
||||
return jsonify({'data': results})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5002)
|
||||
Reference in New Issue
Block a user