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
94 lines
2.5 KiB
Python
94 lines
2.5 KiB
Python
"""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)
|