feature/layouts-csv-refact
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -67,6 +67,7 @@ htmlcov/
|
|||||||
*.dockerignore
|
*.dockerignore
|
||||||
postgres-data/
|
postgres-data/
|
||||||
backend/uploads/
|
backend/uploads/
|
||||||
|
backend/layouts/imports/
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -42,14 +42,24 @@ anexo76/
|
|||||||
│ ├── .env.example # Variables de entorno de ejemplo
|
│ ├── .env.example # Variables de entorno de ejemplo
|
||||||
│ ├── core/ # Módulos core
|
│ ├── core/ # Módulos core
|
||||||
│ │ ├── config.py # Configuración centralizada
|
│ │ ├── config.py # Configuración centralizada
|
||||||
|
│ │ ├── paths.py # Rutas base y layout_path() para importación CSV
|
||||||
│ │ ├── database.py # Configuración de BD multi-tenant
|
│ │ ├── database.py # Configuración de BD multi-tenant
|
||||||
│ │ ├── security.py # Autenticación y autorización
|
│ │ ├── security.py # Autenticación y autorización
|
||||||
│ │ └── middleware.py # Middlewares personalizados
|
│ │ └── middleware.py # Middlewares personalizados
|
||||||
|
│ ├── layouts/ # Directorio de datos: temp y errors de importación CSV (creado al arranque)
|
||||||
│ └── api/
|
│ └── api/
|
||||||
│ └── v1/
|
│ └── v1/
|
||||||
│ ├── router.py # Router principal API v1
|
│ ├── router.py # Router principal API v1
|
||||||
│ └── modules/ # Módulos de negocio
|
│ └── modules/ # Módulos de negocio
|
||||||
│ ├── auth/ # Autenticación
|
│ └── a76/
|
||||||
|
│ ├── layouts_csv/ # Lógica centralizada de cargas por CSV (routes, tasks, validaciones por proceso)
|
||||||
|
│ │ ├── facturas/
|
||||||
|
│ │ ├── customs_brokers/
|
||||||
|
│ │ ├── clients_and_providers/
|
||||||
|
│ │ └── ... # Una carpeta por proceso de importación
|
||||||
|
│ ├── csv_templates/
|
||||||
|
│ └── ...
|
||||||
|
│ ├── auth/
|
||||||
│ ├── tenants/ # Gestión de tenants
|
│ ├── tenants/ # Gestión de tenants
|
||||||
│ ├── licenses/ # Control de licencias
|
│ ├── licenses/ # Control de licencias
|
||||||
│ └── ...
|
│ └── ...
|
||||||
@@ -136,6 +146,11 @@ python -c "from core.database import init_db; init_db()"
|
|||||||
|
|
||||||
```
|
```
|
||||||
docker build -t dev.aduanasoft.com/anexo76/backend:latest -f ./backend/Dockerfile ./backend
|
docker build -t dev.aduanasoft.com/anexo76/backend:latest -f ./backend/Dockerfile ./backend
|
||||||
|
```
|
||||||
|
|
||||||
|
**Importación CSV y workers Celery**: La lógica de cargas por CSV está en `api/v1/modules/a76/layouts_csv/` (una carpeta por proceso). Los workers Celery deben cargar los módulos `api.v1.modules.a76.layouts_csv.<proceso>.tasks`. El directorio de trabajo del worker debe ser la raíz del backend para que `layout_path("imports", "temp")` y `layout_path("imports", "errors")` apunten a los mismos directorios que la API. Si API y worker comparten volumen, los archivos temporales y de errores se escriben en `backend/layouts/imports/temp` y `backend/layouts/imports/errors`.
|
||||||
|
|
||||||
|
```
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg VITE_API_URL=https://anexo76-dev.aduanasoft.com/api/ \
|
--build-arg VITE_API_URL=https://anexo76-dev.aduanasoft.com/api/ \
|
||||||
--build-arg VITE_KEYCLOAK_URL=https://anexo76-dev.aduanasoft.com/kcauth/ \
|
--build-arg VITE_KEYCLOAK_URL=https://anexo76-dev.aduanasoft.com/kcauth/ \
|
||||||
@@ -165,6 +180,8 @@ pip install -r requirements.txt
|
|||||||
uvicorn main:app --reload
|
uvicorn main:app --reload
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Los directorios `backend/layouts/imports/temp` y `backend/layouts/imports/errors` se crean automáticamente al arrancar el backend para la importación CSV.
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Mismo patrón que parts y classes: upload → scan → status → commit.
|
|||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from .imports.routes import router as imports_router
|
from api.v1.modules.a76.layouts_csv.boms.routes import router as imports_router
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_t
|
|||||||
|
|
||||||
from .dto import ClassCreateDTO, ClassCreateDTOFA, ClassResponseDTO, ClassResponseDTOFA, ClassUpdateDTO, ClassWithFADataResponse
|
from .dto import ClassCreateDTO, ClassCreateDTOFA, ClassResponseDTO, ClassResponseDTOFA, ClassUpdateDTO, ClassWithFADataResponse
|
||||||
from .service import ClassService
|
from .service import ClassService
|
||||||
from .imports.routes import router as imports_router
|
from api.v1.modules.a76.layouts_csv.classes.routes import router as imports_router
|
||||||
|
|
||||||
# Create a new router for custom endpoints
|
# Create a new router for custom endpoints
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from .dto import (
|
|||||||
)
|
)
|
||||||
from .service import ClientProviderService
|
from .service import ClientProviderService
|
||||||
from .models import ClientProvider
|
from .models import ClientProvider
|
||||||
from .imports.routes import router as imports_router
|
from api.v1.modules.a76.layouts_csv.clients_and_providers.routes import router as imports_router
|
||||||
|
|
||||||
# Create main router to add custom endpoints
|
# Create main router to add custom endpoints
|
||||||
router = APIRouter(prefix="/clients-providers")
|
router = APIRouter(prefix="/clients-providers")
|
||||||
|
|||||||
@@ -7,35 +7,35 @@ import io
|
|||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
# Importar configs de cada módulo
|
# Importar configs de cada módulo
|
||||||
from api.v1.modules.a76.imports.template_config import (
|
from api.v1.modules.a76.layouts_csv.facturas.template_config import (
|
||||||
TEMPLATE_COLUMNS as IMPORTS_TEMPLATE_COLUMNS,
|
TEMPLATE_COLUMNS as IMPORTS_TEMPLATE_COLUMNS,
|
||||||
_resolve_template_columns as resolve_imports_template,
|
_resolve_template_columns as resolve_imports_template,
|
||||||
)
|
)
|
||||||
from api.v1.modules.a76.parts.imports.template_config import TEMPLATE_COLUMNS as PARTS_TEMPLATE_COLUMNS
|
from api.v1.modules.a76.layouts_csv.parts.template_config import TEMPLATE_COLUMNS as PARTS_TEMPLATE_COLUMNS
|
||||||
from api.v1.modules.a76.boms.imports.template_config import TEMPLATE_COLUMNS as BOMS_TEMPLATE_COLUMNS
|
from api.v1.modules.a76.layouts_csv.boms.template_config import TEMPLATE_COLUMNS as BOMS_TEMPLATE_COLUMNS
|
||||||
from api.v1.modules.a76.classes.imports.template_config import TEMPLATE_COLUMNS as CLASSES_TEMPLATE_COLUMNS
|
from api.v1.modules.a76.layouts_csv.classes.template_config import TEMPLATE_COLUMNS as CLASSES_TEMPLATE_COLUMNS
|
||||||
from api.v1.modules.a76.customs_brokers.imports.template_config import (
|
from api.v1.modules.a76.layouts_csv.customs_brokers.template_config import (
|
||||||
TEMPLATE_COLUMNS as CUSTOMS_BROKERS_TEMPLATE_COLUMNS,
|
TEMPLATE_COLUMNS as CUSTOMS_BROKERS_TEMPLATE_COLUMNS,
|
||||||
)
|
)
|
||||||
from api.v1.modules.a76.clients_and_providers.imports.template_config import (
|
from api.v1.modules.a76.layouts_csv.clients_and_providers.template_config import (
|
||||||
TEMPLATE_COLUMNS as CLIENTS_PROVIDERS_TEMPLATE_COLUMNS,
|
TEMPLATE_COLUMNS as CLIENTS_PROVIDERS_TEMPLATE_COLUMNS,
|
||||||
)
|
)
|
||||||
from api.v1.modules.a76.general_catalogs.exchange_rate.imports.template_config import (
|
from api.v1.modules.a76.layouts_csv.exchange_rate.template_config import (
|
||||||
TEMPLATE_COLUMNS as EXCHANGE_RATE_TEMPLATE_COLUMNS,
|
TEMPLATE_COLUMNS as EXCHANGE_RATE_TEMPLATE_COLUMNS,
|
||||||
)
|
)
|
||||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.imports.template_config import (
|
from api.v1.modules.a76.layouts_csv.us_tariff_fractions.template_config import (
|
||||||
TEMPLATE_COLUMNS as US_TARIFF_FRACTIONS_TEMPLATE_COLUMNS,
|
TEMPLATE_COLUMNS as US_TARIFF_FRACTIONS_TEMPLATE_COLUMNS,
|
||||||
)
|
)
|
||||||
from api.v1.modules.a76.pedmientos.imports.template_config import (
|
from api.v1.modules.a76.layouts_csv.pedmientos.template_config import (
|
||||||
TEMPLATE_COLUMNS as PEDIMENTOS_TEMPLATE_COLUMNS,
|
TEMPLATE_COLUMNS as PEDIMENTOS_TEMPLATE_COLUMNS,
|
||||||
)
|
)
|
||||||
from api.v1.modules.a76.transportation.vehicles.imports.template_config import (
|
from api.v1.modules.a76.layouts_csv.vehicles.template_config import (
|
||||||
TEMPLATE_COLUMNS as VEHICLES_TEMPLATE_COLUMNS,
|
TEMPLATE_COLUMNS as VEHICLES_TEMPLATE_COLUMNS,
|
||||||
)
|
)
|
||||||
from api.v1.modules.a76.transportation.drivers.imports.template_config import (
|
from api.v1.modules.a76.layouts_csv.drivers.template_config import (
|
||||||
TEMPLATE_COLUMNS as DRIVERS_TEMPLATE_COLUMNS,
|
TEMPLATE_COLUMNS as DRIVERS_TEMPLATE_COLUMNS,
|
||||||
)
|
)
|
||||||
from api.v1.modules.a76.transportation.trailers.imports.template_config import (
|
from api.v1.modules.a76.layouts_csv.trailers.template_config import (
|
||||||
TEMPLATE_COLUMNS as TRAILERS_TEMPLATE_COLUMNS,
|
TEMPLATE_COLUMNS as TRAILERS_TEMPLATE_COLUMNS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
# CSV import flow for Agentes Aduanales (Customs Brokers).
|
|
||||||
# Replicates the same two-phase flow as a76.imports: upload → scan → waiting_confirmation → commit.
|
|
||||||
@@ -7,7 +7,7 @@ from core.security import get_current_user, validate_access_to_resource
|
|||||||
|
|
||||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||||
from . import dto, services
|
from . import dto, services
|
||||||
from .imports.routes import router as imports_router
|
from ..layouts_csv.customs_brokers.routes import router as imports_router
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from fastapi import APIRouter
|
|||||||
custom_router = APIRouter(prefix="/exchange-rate", tags=[])
|
custom_router = APIRouter(prefix="/exchange-rate", tags=[])
|
||||||
|
|
||||||
# CSV import: add to custom_router BEFORE including it in master, so /exchange-rate/imports/* is registered
|
# CSV import: add to custom_router BEFORE including it in master, so /exchange-rate/imports/* is registered
|
||||||
from .imports.routes import router as imports_router
|
from api.v1.modules.a76.layouts_csv.exchange_rate.routes import router as imports_router
|
||||||
custom_router.include_router(imports_router, prefix="/imports", tags=["exchange_rate / csv_import"])
|
custom_router.include_router(imports_router, prefix="/imports", tags=["exchange_rate / csv_import"])
|
||||||
|
|
||||||
@custom_router.get("/test-ping")
|
@custom_router.get("/test-ping")
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ crud_router = TenantCRUDRoutes(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Master router with prefix so all routes live under /us-tariff-fractions
|
# Master router with prefix so all routes live under /us-tariff-fractions
|
||||||
from .imports.routes import router as imports_router
|
from api.v1.modules.a76.layouts_csv.us_tariff_fractions.routes import router as imports_router
|
||||||
main_router = APIRouter(prefix="/us-tariff-fractions", tags=["a76 / general catalogs / us tariff fractions"])
|
main_router = APIRouter(prefix="/us-tariff-fractions", tags=["a76 / general catalogs / us tariff fractions"])
|
||||||
main_router.include_router(imports_router, prefix="/imports", tags=["us_tariff_fractions / csv_import"])
|
main_router.include_router(imports_router, prefix="/imports", tags=["us_tariff_fractions / csv_import"])
|
||||||
main_router.include_router(crud_router.router)
|
main_router.include_router(crud_router.router)
|
||||||
|
|||||||
1
backend/api/v1/modules/a76/layouts_csv/__init__.py
Normal file
1
backend/api/v1/modules/a76/layouts_csv/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Lógica centralizada de cargas por CSV (routes, tasks, validaciones, template_config por proceso)
|
||||||
@@ -14,6 +14,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .schemas import ImportJobResponse
|
from .schemas import ImportJobResponse
|
||||||
@@ -78,7 +79,7 @@ async def upload_import_file(
|
|||||||
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
with open(os.path.join(upload_dir, f"bom_{job_id}.csv"), "wb") as f:
|
with open(os.path.join(upload_dir, f"bom_{job_id}.csv"), "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -15,6 +15,7 @@ from typing import Dict, Any, Optional, List, Set
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ def _get_redis():
|
|||||||
|
|
||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -162,7 +163,7 @@ def scan_file(self, job_id: str, config: str = None):
|
|||||||
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(error_dir, exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
error_path = os.path.join(error_dir, f"bom_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"bom_{job_id}.jsonl")
|
||||||
|
|
||||||
@@ -306,8 +307,7 @@ def insert_valid_rows(self, job_id: str):
|
|||||||
else:
|
else:
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
base_dir = os.path.dirname(file_path)
|
error_dir = layout_path("imports", "errors")
|
||||||
error_dir = base_dir.replace("temp", "errors")
|
|
||||||
error_path = os.path.join(error_dir, f"bom_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"bom_{job_id}.jsonl")
|
||||||
|
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -14,6 +14,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .schemas import ImportJobResponse
|
from .schemas import ImportJobResponse
|
||||||
@@ -78,7 +79,7 @@ async def upload_import_file(
|
|||||||
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
with open(os.path.join(upload_dir, f"cls_{job_id}.csv"), "wb") as f:
|
with open(os.path.join(upload_dir, f"cls_{job_id}.csv"), "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -13,6 +13,7 @@ from typing import Dict, Any, Optional, List, Set
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ def _get_redis():
|
|||||||
|
|
||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -156,7 +157,7 @@ def scan_file(self, job_id: str, config: str = None):
|
|||||||
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(error_dir, exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
error_path = os.path.join(error_dir, f"cls_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"cls_{job_id}.jsonl")
|
||||||
|
|
||||||
@@ -310,8 +311,7 @@ def insert_valid_rows(self, job_id: str):
|
|||||||
else:
|
else:
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
base_dir = os.path.dirname(file_path)
|
error_dir = layout_path("imports", "errors")
|
||||||
error_dir = base_dir.replace("temp", "errors")
|
|
||||||
error_path = os.path.join(error_dir, f"cls_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"cls_{job_id}.jsonl")
|
||||||
|
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -14,6 +14,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .schemas import ImportJobResponse
|
from .schemas import ImportJobResponse
|
||||||
@@ -81,7 +82,7 @@ async def upload_import_file(
|
|||||||
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
with open(os.path.join(upload_dir, f"cp_{job_id}.csv"), "wb") as f:
|
with open(os.path.join(upload_dir, f"cp_{job_id}.csv"), "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -13,6 +13,7 @@ from typing import Dict, Any, Optional, List
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
from api.v1.modules.a76.clients_and_providers.models import (
|
from api.v1.modules.a76.clients_and_providers.models import (
|
||||||
@@ -37,7 +38,7 @@ def _get_redis():
|
|||||||
|
|
||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -165,7 +166,7 @@ def scan_file(self, job_id: str, config: str = None):
|
|||||||
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(error_dir, exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
error_path = os.path.join(error_dir, f"cp_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"cp_{job_id}.jsonl")
|
||||||
|
|
||||||
@@ -290,8 +291,7 @@ def insert_valid_rows(self, job_id: str):
|
|||||||
else:
|
else:
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
base_dir = os.path.dirname(file_path)
|
error_dir = layout_path("imports", "errors")
|
||||||
error_dir = base_dir.replace("temp", "errors")
|
|
||||||
error_path = os.path.join(error_dir, f"cp_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"cp_{job_id}.jsonl")
|
||||||
|
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# layouts_csv.customs_brokers
|
||||||
@@ -14,6 +14,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .schemas import ImportJobResponse
|
from .schemas import ImportJobResponse
|
||||||
@@ -81,7 +82,7 @@ async def upload_import_file(
|
|||||||
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
with open(os.path.join(upload_dir, f"cb_{job_id}.csv"), "wb") as f:
|
with open(os.path.join(upload_dir, f"cb_{job_id}.csv"), "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -13,6 +13,7 @@ from typing import Dict, Any, Optional, List
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ def _get_redis():
|
|||||||
|
|
||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -120,7 +121,7 @@ def scan_file(self, job_id: str, config: str = None):
|
|||||||
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(error_dir, exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
error_path = os.path.join(error_dir, f"cb_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"cb_{job_id}.jsonl")
|
||||||
|
|
||||||
@@ -245,8 +246,7 @@ def insert_valid_rows(self, job_id: str):
|
|||||||
else:
|
else:
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
base_dir = os.path.dirname(file_path)
|
error_dir = layout_path("imports", "errors")
|
||||||
error_dir = base_dir.replace("temp", "errors")
|
|
||||||
error_path = os.path.join(error_dir, f"cb_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"cb_{job_id}.jsonl")
|
||||||
|
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -15,6 +15,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .schemas import ImportJobResponse
|
from .schemas import ImportJobResponse
|
||||||
@@ -81,7 +82,7 @@ async def upload_import_file(
|
|||||||
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
with open(os.path.join(upload_dir, f"drv_{job_id}.csv"), "wb") as f:
|
with open(os.path.join(upload_dir, f"drv_{job_id}.csv"), "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -13,6 +13,7 @@ from typing import Dict, Any, Optional, List
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ def _get_redis():
|
|||||||
|
|
||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -281,7 +282,7 @@ def _do_scan(job_id: str, progress_callback: Optional[Any] = None) -> Dict[str,
|
|||||||
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(error_dir, exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
error_path = os.path.join(error_dir, f"drv_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"drv_{job_id}.jsonl")
|
||||||
|
|
||||||
@@ -429,8 +430,7 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
|
|||||||
else:
|
else:
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
base_dir = os.path.dirname(file_path)
|
error_dir = layout_path("imports", "errors")
|
||||||
error_dir = base_dir.replace("temp", "errors")
|
|
||||||
error_path = os.path.join(error_dir, f"drv_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"drv_{job_id}.jsonl")
|
||||||
|
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -14,6 +14,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .schemas import ImportJobResponse
|
from .schemas import ImportJobResponse
|
||||||
@@ -81,7 +82,7 @@ async def upload_import_file(
|
|||||||
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
with open(os.path.join(upload_dir, f"er_{job_id}.csv"), "wb") as f:
|
with open(os.path.join(upload_dir, f"er_{job_id}.csv"), "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -15,6 +15,7 @@ from typing import Dict, Any, Optional, List
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ def _get_redis():
|
|||||||
|
|
||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -150,7 +151,7 @@ def scan_file(self, job_id: str, config: str = None):
|
|||||||
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(error_dir, exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
error_path = os.path.join(error_dir, f"er_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"er_{job_id}.jsonl")
|
||||||
|
|
||||||
@@ -274,8 +275,7 @@ def insert_valid_rows(self, job_id: str):
|
|||||||
else:
|
else:
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
base_dir = os.path.dirname(file_path)
|
error_dir = layout_path("imports", "errors")
|
||||||
error_dir = base_dir.replace("temp", "errors")
|
|
||||||
error_path = os.path.join(error_dir, f"er_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"er_{job_id}.jsonl")
|
||||||
|
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -11,6 +11,7 @@ from typing import Optional, Literal, Dict, Any
|
|||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.config import settings
|
from core.config import settings
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .tasks import (
|
from .tasks import (
|
||||||
@@ -88,7 +89,7 @@ async def upload_import_file(
|
|||||||
|
|
||||||
# Optional: also write to local disk (e.g. for same-machine worker or debugging)
|
# Optional: also write to local disk (e.g. for same-machine worker or debugging)
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
file_path = os.path.join(upload_dir, f"{job_id}.csv")
|
file_path = os.path.join(upload_dir, f"{job_id}.csv")
|
||||||
meta_path = os.path.join(upload_dir, f"{job_id}.meta.json")
|
meta_path = os.path.join(upload_dir, f"{job_id}.meta.json")
|
||||||
@@ -11,6 +11,7 @@ from typing import Dict, Any, Optional, List
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
# Models are imported inside tasks to avoid circular dependencies and mapper initialization issues in the API process
|
# Models are imported inside tasks to avoid circular dependencies and mapper initialization issues in the API process
|
||||||
@@ -37,7 +38,7 @@ def _get_redis():
|
|||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
"""Directory on the worker for temp CSV and meta (same structure as API, but local to worker)."""
|
"""Directory on the worker for temp CSV and meta (same structure as API, but local to worker)."""
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -189,8 +190,9 @@ def scan_file(self, job_id: str, model_target: str, config: str = None):
|
|||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
# 2. Setup Error Log
|
# 2. Setup Error Log
|
||||||
error_path = file_path.replace("temp", "errors").replace(".csv", ".jsonl")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(os.path.dirname(error_path), exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
|
error_path = os.path.join(error_dir, f"{job_id}.jsonl")
|
||||||
|
|
||||||
total_rows = 0
|
total_rows = 0
|
||||||
error_count = 0
|
error_count = 0
|
||||||
@@ -805,7 +807,8 @@ def insert_valid_rows(self, job_id: str, model_target: str):
|
|||||||
from api.v1.modules.a76.items.line_descriptions.models import LineDescription
|
from api.v1.modules.a76.items.line_descriptions.models import LineDescription
|
||||||
from api.v1.modules.a76.parts.models import Part
|
from api.v1.modules.a76.parts.models import Part
|
||||||
|
|
||||||
error_path = file_path.replace("temp", "errors").replace(".csv", ".jsonl")
|
error_dir = layout_path("imports", "errors")
|
||||||
|
error_path = os.path.join(error_dir, f"{job_id}.jsonl")
|
||||||
|
|
||||||
# 1. Load Error Line Numbers (from Redis if scan ran on another worker, else from file)
|
# 1. Load Error Line Numbers (from Redis if scan ran on another worker, else from file)
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -14,6 +14,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .schemas import ImportJobResponse
|
from .schemas import ImportJobResponse
|
||||||
@@ -78,7 +79,7 @@ async def upload_import_file(
|
|||||||
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
with open(os.path.join(upload_dir, f"part_{job_id}.csv"), "wb") as f:
|
with open(os.path.join(upload_dir, f"part_{job_id}.csv"), "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -14,6 +14,7 @@ from typing import Dict, Any, Optional, List, Set
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ def _get_redis():
|
|||||||
|
|
||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -183,7 +184,7 @@ def scan_file(self, job_id: str, config: str = None):
|
|||||||
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(error_dir, exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
error_path = os.path.join(error_dir, f"part_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"part_{job_id}.jsonl")
|
||||||
|
|
||||||
@@ -368,8 +369,7 @@ def insert_valid_rows(self, job_id: str):
|
|||||||
else:
|
else:
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
base_dir = os.path.dirname(file_path)
|
error_dir = layout_path("imports", "errors")
|
||||||
error_dir = base_dir.replace("temp", "errors")
|
|
||||||
error_path = os.path.join(error_dir, f"part_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"part_{job_id}.jsonl")
|
||||||
|
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -14,6 +14,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .schemas import ImportJobResponse
|
from .schemas import ImportJobResponse
|
||||||
@@ -81,7 +82,7 @@ async def upload_import_file(
|
|||||||
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
with open(os.path.join(upload_dir, f"ped_{job_id}.csv"), "wb") as f:
|
with open(os.path.join(upload_dir, f"ped_{job_id}.csv"), "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -15,6 +15,7 @@ from typing import Dict, Any, Optional, List, Set
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ def _get_redis():
|
|||||||
|
|
||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -243,7 +244,7 @@ def scan_file(self, job_id: str, config: str = None):
|
|||||||
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(error_dir, exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
error_path = os.path.join(error_dir, f"ped_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"ped_{job_id}.jsonl")
|
||||||
|
|
||||||
@@ -381,8 +382,7 @@ def insert_valid_rows(self, job_id: str):
|
|||||||
else:
|
else:
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
base_dir = os.path.dirname(file_path)
|
error_dir = layout_path("imports", "errors")
|
||||||
error_dir = base_dir.replace("temp", "errors")
|
|
||||||
error_path = os.path.join(error_dir, f"ped_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"ped_{job_id}.jsonl")
|
||||||
|
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -15,6 +15,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .schemas import ImportJobResponse
|
from .schemas import ImportJobResponse
|
||||||
@@ -81,7 +82,7 @@ async def upload_import_file(
|
|||||||
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
with open(os.path.join(upload_dir, f"trl_{job_id}.csv"), "wb") as f:
|
with open(os.path.join(upload_dir, f"trl_{job_id}.csv"), "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -14,6 +14,7 @@ from typing import Dict, Any, Optional, List
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ def _get_redis():
|
|||||||
|
|
||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -180,7 +181,7 @@ def _do_scan(job_id: str, progress_callback: Optional[Any] = None) -> Dict[str,
|
|||||||
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(error_dir, exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
error_path = os.path.join(error_dir, f"trl_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"trl_{job_id}.jsonl")
|
||||||
|
|
||||||
@@ -328,8 +329,7 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
|
|||||||
else:
|
else:
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
base_dir = os.path.dirname(file_path)
|
error_dir = layout_path("imports", "errors")
|
||||||
error_dir = base_dir.replace("temp", "errors")
|
|
||||||
error_path = os.path.join(error_dir, f"trl_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"trl_{job_id}.jsonl")
|
||||||
|
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -14,6 +14,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .schemas import ImportJobResponse
|
from .schemas import ImportJobResponse
|
||||||
@@ -81,7 +82,7 @@ async def upload_import_file(
|
|||||||
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
with open(os.path.join(upload_dir, f"fa_{job_id}.csv"), "wb") as f:
|
with open(os.path.join(upload_dir, f"fa_{job_id}.csv"), "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -14,6 +14,7 @@ from typing import Dict, Any, Optional, List
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ def _get_redis():
|
|||||||
|
|
||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -157,7 +158,7 @@ def scan_file(self, job_id: str, config: str = None):
|
|||||||
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(error_dir, exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
error_path = os.path.join(error_dir, f"fa_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"fa_{job_id}.jsonl")
|
||||||
|
|
||||||
@@ -290,8 +291,7 @@ def insert_valid_rows(self, job_id: str):
|
|||||||
else:
|
else:
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
base_dir = os.path.dirname(file_path)
|
error_dir = layout_path("imports", "errors")
|
||||||
error_dir = base_dir.replace("temp", "errors")
|
|
||||||
error_path = os.path.join(error_dir, f"fa_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"fa_{job_id}.jsonl")
|
||||||
|
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -15,6 +15,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
|
||||||
from .schemas import ImportJobResponse
|
from .schemas import ImportJobResponse
|
||||||
@@ -81,7 +82,7 @@ async def upload_import_file(
|
|||||||
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
|
upload_dir = layout_path("imports", "temp")
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
with open(os.path.join(upload_dir, f"veh_{job_id}.csv"), "wb") as f:
|
with open(os.path.join(upload_dir, f"veh_{job_id}.csv"), "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -15,6 +15,7 @@ from typing import Dict, Any, Optional, List
|
|||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
|
from core.paths import layout_path
|
||||||
|
|
||||||
from .template_config import row_from_template
|
from .template_config import row_from_template
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ def _get_redis():
|
|||||||
|
|
||||||
|
|
||||||
def _worker_upload_dir() -> str:
|
def _worker_upload_dir() -> str:
|
||||||
return os.path.join(os.getcwd(), "uploads", "temp")
|
return layout_path("imports", "temp")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
|
||||||
@@ -213,7 +214,7 @@ def _do_scan(
|
|||||||
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
|
error_dir = layout_path("imports", "errors")
|
||||||
os.makedirs(error_dir, exist_ok=True)
|
os.makedirs(error_dir, exist_ok=True)
|
||||||
error_path = os.path.join(error_dir, f"veh_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"veh_{job_id}.jsonl")
|
||||||
|
|
||||||
@@ -404,8 +405,7 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
|
|||||||
else:
|
else:
|
||||||
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
_ensure_worker_has_meta_from_redis(job_id, file_path)
|
||||||
|
|
||||||
base_dir = os.path.dirname(file_path)
|
error_dir = layout_path("imports", "errors")
|
||||||
error_dir = base_dir.replace("temp", "errors")
|
|
||||||
error_path = os.path.join(error_dir, f"veh_{job_id}.jsonl")
|
error_path = os.path.join(error_dir, f"veh_{job_id}.jsonl")
|
||||||
|
|
||||||
error_lines = set()
|
error_lines = set()
|
||||||
@@ -8,7 +8,7 @@ from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
|||||||
|
|
||||||
from .dto import PartCreateDTO, PartResponseDTO, PartUpdateDTO
|
from .dto import PartCreateDTO, PartResponseDTO, PartUpdateDTO
|
||||||
from .service import PartService
|
from .service import PartService
|
||||||
from .imports.routes import router as imports_router
|
from api.v1.modules.a76.layouts_csv.parts.routes import router as imports_router
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from .routes.pedimento_rectification_origin import (
|
|||||||
from .routes.pedimento_transport_means import router as pedimento_transport_means_router
|
from .routes.pedimento_transport_means import router as pedimento_transport_means_router
|
||||||
from .routes.pedimento_validation import router as pedimento_validation_router
|
from .routes.pedimento_validation import router as pedimento_validation_router
|
||||||
from .routes.pedimentos import router as pedimentos_router
|
from .routes.pedimentos import router as pedimentos_router
|
||||||
from .imports.routes import router as pedimentos_imports_router
|
from api.v1.modules.a76.layouts_csv.pedmientos.routes import router as pedimentos_imports_router
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from .items.routes import router as items_router
|
|||||||
from .classes import router as classes_router
|
from .classes import router as classes_router
|
||||||
|
|
||||||
from .clients_and_providers import router as client_and_provider_router
|
from .clients_and_providers import router as client_and_provider_router
|
||||||
from .imports.routes import router as imports_router
|
from .layouts_csv.facturas.routes import router as imports_router
|
||||||
from .csv_templates.routes import router as csv_templates_router
|
from .csv_templates.routes import router as csv_templates_router
|
||||||
from .invoice_settings.routes import router as invoice_settings_router
|
from .invoice_settings.routes import router as invoice_settings_router
|
||||||
from .item_presets.routes import router as item_presets_router
|
from .item_presets.routes import router as item_presets_router
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
|
|||||||
from .dto import DriverCreateDTO, DriverResponseDTO
|
from .dto import DriverCreateDTO, DriverResponseDTO
|
||||||
from .models import Driver
|
from .models import Driver
|
||||||
from .services import DriverService
|
from .services import DriverService
|
||||||
from .imports.routes import router as imports_router
|
from api.v1.modules.a76.layouts_csv.drivers.routes import router as imports_router
|
||||||
|
|
||||||
router = APIRouter(prefix="/drivers")
|
router = APIRouter(prefix="/drivers")
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
|||||||
|
|
||||||
from .dto import TrailerCreateDTO, TrailerResponseDTO, TrailerUpdateDTO
|
from .dto import TrailerCreateDTO, TrailerResponseDTO, TrailerUpdateDTO
|
||||||
from .services import TrailerService
|
from .services import TrailerService
|
||||||
from .imports.routes import router as imports_router
|
from api.v1.modules.a76.layouts_csv.trailers.routes import router as imports_router
|
||||||
|
|
||||||
# Main router: trailers CRUD + CSV imports
|
# Main router: trailers CRUD + CSV imports
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
|||||||
|
|
||||||
from .dto import VehicleCreateDTO, VehicleResponseDTO, VehicleUpdateDTO
|
from .dto import VehicleCreateDTO, VehicleResponseDTO, VehicleUpdateDTO
|
||||||
from .services import VehicleService
|
from .services import VehicleService
|
||||||
from .imports.routes import router as imports_router
|
from api.v1.modules.a76.layouts_csv.vehicles.routes import router as imports_router
|
||||||
|
|
||||||
# Main router: vehicles CRUD + CSV imports
|
# Main router: vehicles CRUD + CSV imports
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|||||||
Binary file not shown.
@@ -32,16 +32,18 @@ celery_app.conf.update(
|
|||||||
"api.v1.modules.a76.reports.exportacion.aviso_consolidado.task",
|
"api.v1.modules.a76.reports.exportacion.aviso_consolidado.task",
|
||||||
"api.v1.modules.a76.reports.movements.invoices.tasks",
|
"api.v1.modules.a76.reports.movements.invoices.tasks",
|
||||||
"api.v1.modules.a76.reports.exportacion.descargo.task",
|
"api.v1.modules.a76.reports.exportacion.descargo.task",
|
||||||
"api.v1.modules.a76.imports.tasks",
|
"api.v1.modules.a76.layouts_csv.facturas.tasks",
|
||||||
"api.v1.modules.a76.customs_brokers.imports.tasks",
|
"api.v1.modules.a76.layouts_csv.customs_brokers.tasks",
|
||||||
"api.v1.modules.a76.clients_and_providers.imports.tasks",
|
"api.v1.modules.a76.layouts_csv.clients_and_providers.tasks",
|
||||||
"api.v1.modules.a76.pedmientos.imports.tasks",
|
"api.v1.modules.a76.layouts_csv.pedmientos.tasks",
|
||||||
"api.v1.modules.a76.general_catalogs.exchange_rate.imports.tasks",
|
"api.v1.modules.a76.layouts_csv.exchange_rate.tasks",
|
||||||
"api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.imports.tasks",
|
"api.v1.modules.a76.layouts_csv.us_tariff_fractions.tasks",
|
||||||
"api.v1.modules.a76.classes.imports.tasks",
|
"api.v1.modules.a76.layouts_csv.classes.tasks",
|
||||||
"api.v1.modules.a76.parts.imports.tasks",
|
"api.v1.modules.a76.layouts_csv.parts.tasks",
|
||||||
"api.v1.modules.a76.boms.imports.tasks",
|
"api.v1.modules.a76.layouts_csv.boms.tasks",
|
||||||
"api.v1.modules.a76.transportation.vehicles.imports.tasks",
|
"api.v1.modules.a76.layouts_csv.vehicles.tasks",
|
||||||
|
"api.v1.modules.a76.layouts_csv.drivers.tasks",
|
||||||
|
"api.v1.modules.a76.layouts_csv.trailers.tasks",
|
||||||
"api.v1.modules.a76.reports.exportacion.transmission.MAINX30.task",
|
"api.v1.modules.a76.reports.exportacion.transmission.MAINX30.task",
|
||||||
"api.v1.modules.a76.reports.importacion.transmission.temporal.MAINX30.task",
|
"api.v1.modules.a76.reports.importacion.transmission.temporal.MAINX30.task",
|
||||||
"api.v1.modules.a76.reports.importacion.transmission.definitive.MAINX30.task",
|
"api.v1.modules.a76.reports.importacion.transmission.definitive.MAINX30.task",
|
||||||
|
|||||||
15
backend/core/paths.py
Normal file
15
backend/core/paths.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"""
|
||||||
|
Rutas base y resolución de paths para layouts (importación CSV, temp, errors).
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Raíz del backend (directorio que contiene api/, core/, etc.)
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def layout_path(*parts: str) -> str:
|
||||||
|
"""Construye una ruta absoluta bajo backend/layouts/."""
|
||||||
|
p = BASE_DIR / "layouts"
|
||||||
|
for part in parts:
|
||||||
|
p = p / part
|
||||||
|
return str(p)
|
||||||
@@ -89,6 +89,7 @@ import core.celery_app # Initialize Celery App
|
|||||||
from api.v1.router import router as api_v1_router
|
from api.v1.router import router as api_v1_router
|
||||||
from core.config import settings
|
from core.config import settings
|
||||||
from core.database import init_db
|
from core.database import init_db
|
||||||
|
from core.paths import layout_path
|
||||||
from core.error_handlers import register_exception_handlers
|
from core.error_handlers import register_exception_handlers
|
||||||
from core.middleware import (
|
from core.middleware import (
|
||||||
LicenseValidationMiddleware,
|
LicenseValidationMiddleware,
|
||||||
@@ -359,6 +360,9 @@ uploads_dir = Path("uploads").resolve()
|
|||||||
uploads_dir.mkdir(parents=True, exist_ok=True)
|
uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||||
app.mount("/api/uploads", StaticFiles(directory=str(uploads_dir)), name="uploads")
|
app.mount("/api/uploads", StaticFiles(directory=str(uploads_dir)), name="uploads")
|
||||||
|
|
||||||
|
# Directorios para importación CSV (layouts: temp y errors)
|
||||||
|
Path(layout_path("imports", "temp")).mkdir(parents=True, exist_ok=True)
|
||||||
|
Path(layout_path("imports", "errors")).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Registrar routers
|
# Registrar routers
|
||||||
app.include_router(api_v1_router, prefix="/api/v1")
|
app.include_router(api_v1_router, prefix="/api/v1")
|
||||||
|
|||||||
Reference in New Issue
Block a user