fk_util_tools 0.1.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. fk_util_tools-0.1.14/LICENSE +21 -0
  2. fk_util_tools-0.1.14/PKG-INFO +31 -0
  3. fk_util_tools-0.1.14/fk_utils/__init__.py +3 -0
  4. fk_util_tools-0.1.14/fk_utils/cache/__init__.py +0 -0
  5. fk_util_tools-0.1.14/fk_utils/cache/redis.py +85 -0
  6. fk_util_tools-0.1.14/fk_utils/config.py +24 -0
  7. fk_util_tools-0.1.14/fk_utils/envs/__init__.py +0 -0
  8. fk_util_tools-0.1.14/fk_utils/envs/aws/__init__.py +0 -0
  9. fk_util_tools-0.1.14/fk_utils/envs/aws/parameters.py +41 -0
  10. fk_util_tools-0.1.14/fk_utils/envs/aws/secrets.py +31 -0
  11. fk_util_tools-0.1.14/fk_utils/exceptions/__init__.py +0 -0
  12. fk_util_tools-0.1.14/fk_utils/exceptions/custom_http_exception.py +12 -0
  13. fk_util_tools-0.1.14/fk_utils/exceptions/custom_http_exception.pyi +8 -0
  14. fk_util_tools-0.1.14/fk_utils/logging/__init__.py +0 -0
  15. fk_util_tools-0.1.14/fk_utils/logging/fastapi/__init__.py +0 -0
  16. fk_util_tools-0.1.14/fk_utils/logging/fastapi/logging.py +56 -0
  17. fk_util_tools-0.1.14/fk_utils/middlewares/__init__.py +0 -0
  18. fk_util_tools-0.1.14/fk_utils/middlewares/django/__init__.py +0 -0
  19. fk_util_tools-0.1.14/fk_utils/middlewares/django/handler.py +0 -0
  20. fk_util_tools-0.1.14/fk_utils/middlewares/django/sql_middleware.py +60 -0
  21. fk_util_tools-0.1.14/fk_utils/middlewares/fastapi/__init__.py +0 -0
  22. fk_util_tools-0.1.14/fk_utils/middlewares/fastapi/error_middleware.py +128 -0
  23. fk_util_tools-0.1.14/fk_utils/middlewares/fastapi/i18n.py +66 -0
  24. fk_util_tools-0.1.14/fk_utils/middlewares/fastapi/language_middleware.py +21 -0
  25. fk_util_tools-0.1.14/fk_utils/middlewares/fastapi/sql_middleware.py +70 -0
  26. fk_util_tools-0.1.14/fk_utils/middlewares/flask/__init__.py +0 -0
  27. fk_util_tools-0.1.14/fk_utils/middlewares/flask/handler.py +0 -0
  28. fk_util_tools-0.1.14/fk_utils/middlewares/flask/sql_middleware.py +66 -0
  29. fk_util_tools-0.1.14/fk_utils/py.typed +0 -0
  30. fk_util_tools-0.1.14/fk_utils/traces/__init__.py +0 -0
  31. fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/__init__.py +0 -0
  32. fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/django/__init__.py +0 -0
  33. fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/django/trace.py +68 -0
  34. fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/fastapi/__init__.py +0 -0
  35. fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/fastapi/trace.py +56 -0
  36. fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/flask/__init__.py +0 -0
  37. fk_util_tools-0.1.14/fk_utils/traces/opentelemetry/flask/trace.py +59 -0
  38. fk_util_tools-0.1.14/pyproject.toml +28 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Finkargo Tech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.3
2
+ Name: fk_util_tools
3
+ Version: 0.1.14
4
+ Summary: Herramientas de utilidad o funciones comunes para los proyectos de FK.
5
+ License: MIT
6
+ Author: Steven Santacruz Garcia
7
+ Author-email: stevengarcia1118@gmail.com
8
+ Requires-Python: >=3.7,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Dist: boto3
19
+ Requires-Dist: opentelemetry-api
20
+ Requires-Dist: opentelemetry-exporter-jaeger
21
+ Requires-Dist: opentelemetry-exporter-otlp
22
+ Requires-Dist: opentelemetry-instrumentation-django
23
+ Requires-Dist: opentelemetry-instrumentation-fastapi
24
+ Requires-Dist: opentelemetry-instrumentation-flask
25
+ Requires-Dist: opentelemetry-instrumentation-psycopg2
26
+ Requires-Dist: opentelemetry-instrumentation-requests
27
+ Requires-Dist: opentelemetry-instrumentation-sqlalchemy
28
+ Requires-Dist: opentelemetry-sdk
29
+ Requires-Dist: psycopg2-binary
30
+ Requires-Dist: starlette
31
+ Project-URL: Homepage, https://gitlab.com/f3315/team-back-end/cross/packages/package_utils
@@ -0,0 +1,3 @@
1
+ from . import config
2
+
3
+ SETTINGS = config.Config()
File without changes
@@ -0,0 +1,85 @@
1
+ import base64
2
+ import json
3
+ import redis
4
+ import threading
5
+ from typing import Any, Dict, List, Optional, Union
6
+
7
+
8
+ class CacheService:
9
+ _instance = None
10
+ _lock = threading.Lock()
11
+
12
+ def __new__(cls, redis_url: str):
13
+ if not cls._instance:
14
+ with cls._lock:
15
+ if not cls._instance:
16
+ cls._instance = super(CacheService, cls).__new__(cls)
17
+ cls._instance.__init_instance__(redis_url)
18
+ return cls._instance
19
+
20
+ def __init_instance__(self, redis_url: str):
21
+ """ Actual initialization of the singleton instance """
22
+ self.redis_url = redis_url
23
+ self.__connect__()
24
+
25
+ def __connect__(self):
26
+ """ Establishes the connection with Redis and handles connection errors """
27
+ try:
28
+ print(f"Connecting to Redis at {self.redis_url}...")
29
+ self._cache = redis.Redis.from_url(self.redis_url, decode_responses=True)
30
+ self._cache.ping()
31
+ except redis.ConnectionError as e:
32
+ print(f"Error de conexión a Redis: {e}")
33
+ self._cache = None
34
+
35
+ def _ensure_connection(self):
36
+ """ Reconnects if necessary """
37
+ if not self._cache or not self._cache.ping():
38
+ print("Reconnecting to Redis...")
39
+ self.__connect__()
40
+
41
+ def add(
42
+ self, key: str, row: Union[List[Any], Dict[str, Any]], expiration_time: Optional[int] = None
43
+ ):
44
+ assert isinstance(key, str), "Only str keys are supported!"
45
+ assert isinstance(row, (list, dict)), "Only dict or list objects are supported!"
46
+
47
+ self._ensure_connection()
48
+ if not self._cache:
49
+ print("Could not establish connection with Redis.")
50
+ return
51
+
52
+ raw = {
53
+ "content": base64.b64encode(json.dumps(row).encode("ascii")).decode("ascii")
54
+ }
55
+ self._cache.hset(key, mapping=raw)
56
+ if expiration_time:
57
+ self._cache.expire(key, expiration_time)
58
+
59
+ def get(self, key: str) -> Optional[Union[List[Any], Dict[str, Any]]]:
60
+ assert isinstance(key, str), "Only str keys are supported!"
61
+
62
+ self._ensure_connection()
63
+ if not self._cache:
64
+ print("Could not establish connection with Redis.")
65
+ return None
66
+
67
+ row = self._cache.hgetall(key)
68
+ if not row:
69
+ return None
70
+
71
+ content = row.get("content")
72
+ if content is None:
73
+ return None
74
+ return json.loads(base64.b64decode(content).decode("ascii"))
75
+
76
+ def delete(self, key: str):
77
+ assert isinstance(key, str), "Only str keys are supported!"
78
+
79
+ self._ensure_connection()
80
+ if not self._cache:
81
+ print("Could not establish connection with Redis.")
82
+ return
83
+
84
+ if self._cache.exists(key):
85
+ self._cache.delete(key)
@@ -0,0 +1,24 @@
1
+ import os
2
+
3
+
4
+ class Config:
5
+ def __init__(self):
6
+ # Environment
7
+ self.DD_ENV = os.environ.get('DD_ENV', 'localhost')
8
+ self.DEBUG_UTILS = os.environ.get('DEBUG_UTILS', False)
9
+ self.REPOSITORY = os.environ.get('REPOSITORY', 'msa_repository')
10
+ self.TYPE_DEPLOY = os.environ.get('TYPE_DEPLOY', 'api')
11
+
12
+ # AWS
13
+ self.AWS_PROFILE_NAME = os.environ.get('AWS_PROFILE_NAME', 'default')
14
+ self.AWS_REGION_NAME = os.environ.get('AWS_REGION_NAME', 'us-east-1')
15
+ self.SECRET_NAME = os.environ.get('SECRET_NAME', 'api/testing/ms-communicator')
16
+
17
+ # Opentelemtry
18
+ self.OTLP_HOST = os.environ.get('OTLP_HOST', 'localhost')
19
+ self.OTLP_PORT = os.environ.get('OTLP_PORT', '0000')
20
+ self.OTLP_NAME = os.environ.get('OTLP_NAME', 'ms-localhost')
21
+
22
+ # DataDog
23
+ self.DD_HOST = os.environ.get('DD_HOST', 'localhost')
24
+ self.DD_PORT = os.environ.get('DD_PORT', '8126')
File without changes
File without changes
@@ -0,0 +1,41 @@
1
+ import boto3
2
+ import os
3
+ import logging
4
+ from typing import Optional
5
+ from fk_utils import SETTINGS
6
+
7
+ # Configuración del logger
8
+ logging.basicConfig(level=logging.ERROR)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Crear clientes de Boto3 fuera de las funciones
12
+ ssm_client = boto3.client('ssm', region_name=SETTINGS.AWS_REGION_NAME)
13
+
14
+
15
+ def get_parameter(param: str, param_type: str) -> Optional[str]:
16
+ """
17
+ Obtiene un parámetro de AWS Systems Manager Parameter Store.
18
+
19
+ Args:
20
+ param (str): Nombre del parámetro.
21
+ param_type (str): Tipo de parámetro (static, global, static_env, default).
22
+
23
+ Returns:
24
+ str: Valor del parámetro o 'None' si hay un error.
25
+ """
26
+ try:
27
+ param_mappings = {
28
+ 'static': f"/ecs/{SETTINGS.REPOSITORY}_{SETTINGS.TYPE_DEPLOY}/{param}",
29
+ 'global': f"/ecs/global/{param}",
30
+ 'static_env': f"/ecs/{SETTINGS.DD_ENV}/{param}",
31
+ 'default': f"/ecs/{SETTINGS.DD_ENV}/{SETTINGS.REPOSITORY}_{SETTINGS.TYPE_DEPLOY}/{param}"
32
+ }
33
+
34
+ param_name = param_mappings.get(param_type, param_mappings['default'])
35
+
36
+ response = ssm_client.get_parameter(Name=param_name, WithDecryption=True)
37
+ return response['Parameter']['Value']
38
+ except Exception as e:
39
+ logger.error(f'Error al obtener el parámetro en AWS: {str(e)}')
40
+
41
+ return os.environ.get(param, 'None')
@@ -0,0 +1,31 @@
1
+ import boto3
2
+ import json
3
+ import logging
4
+ from typing import Optional
5
+ from fk_utils import SETTINGS
6
+
7
+ # Configuración del logger
8
+ logging.basicConfig(level=logging.ERROR)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Crear clientes de Boto3 fuera de las funciones
12
+ secrets_manager_client = boto3.client('secretsmanager', region_name=SETTINGS.AWS_REGION_NAME)
13
+
14
+
15
+ def get_secret_key() -> Optional[dict]:
16
+ """
17
+ Obtiene la clave secreta almacenada en AWS Secrets Manager.
18
+
19
+ Returns:
20
+ dict or None: Diccionario con la clave secreta o None si hay un error.
21
+ """
22
+ try:
23
+ response = secrets_manager_client.get_secret_value(SecretId=SETTINGS.SECRET_NAME)
24
+ secret_value = response.get('SecretString')
25
+
26
+ if secret_value:
27
+ return json.loads(secret_value)
28
+ except Exception as e:
29
+ logger.error(f'Error al obtener el secreto: {str(e)}')
30
+
31
+ return None
File without changes
@@ -0,0 +1,12 @@
1
+ from fastapi import HTTPException
2
+ from enum import Enum
3
+ from typing import TypeVar, Optional
4
+
5
+ T = TypeVar("T", bound=Enum)
6
+
7
+
8
+ class CustomHTTPException(HTTPException):
9
+ def __init__(self, error_code: T, detail: Optional[str] = None):
10
+ super().__init__(status_code=getattr(error_code, "status_code", 500),
11
+ detail=detail or getattr(error_code, "detail", "Unknown error"))
12
+ self.error_code = error_code
@@ -0,0 +1,8 @@
1
+ from fastapi import HTTPException
2
+ from enum import Enum
3
+ from typing import TypeVar, Optional
4
+
5
+ T = TypeVar("T", bound=Enum)
6
+
7
+ class CustomHTTPException(HTTPException):
8
+ def __init__(self, error_code: T, detail: Optional[str] = None) -> None: ...
File without changes
@@ -0,0 +1,56 @@
1
+ import logging
2
+ from opentelemetry import trace
3
+ from opentelemetry.sdk.trace import TracerProvider
4
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
5
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
6
+ from opentelemetry.sdk.resources import Resource
7
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
8
+ from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
9
+ from opentelemetry.instrumentation.requests import RequestsInstrumentor
10
+
11
+ from fk_utils import SETTINGS
12
+
13
+ # Configurar logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def configure_tracer():
19
+ try:
20
+ # Configurar el proveedor de trazas
21
+ trace.set_tracer_provider(TracerProvider(
22
+ resource=Resource.create({"service.name": SETTINGS.OTLP_NAME})
23
+ ))
24
+
25
+ # Configurar el exportador OTLP
26
+ otlp_exporter = OTLPSpanExporter(
27
+ endpoint=f"{SETTINGS.OTLP_HOST}:{SETTINGS.OTLP_PORT}",
28
+ insecure=True
29
+ )
30
+
31
+ # Añadir el procesador de spans
32
+ trace.get_tracer_provider().add_span_processor(
33
+ BatchSpanProcessor(otlp_exporter)
34
+ )
35
+
36
+ logger.info("OpenTelemetry tracer configurado correctamente.")
37
+
38
+ except Exception as e:
39
+ logger.error(f"Error al configurar el tracer de OpenTelemetry: {e}")
40
+
41
+
42
+ def instrument_app(app):
43
+ try:
44
+ # Instrumentar FastAPI
45
+ FastAPIInstrumentor.instrument_app(app)
46
+
47
+ # Instrumentar SQLAlchemy
48
+ SQLAlchemyInstrumentor().instrument()
49
+
50
+ # Instrumentar Requests
51
+ RequestsInstrumentor().instrument()
52
+
53
+ logger.info("Aplicación instrumentada correctamente con OpenTelemetry.")
54
+
55
+ except Exception as e:
56
+ logger.error(f"Error al instrumentar la aplicación: {e}")
File without changes
@@ -0,0 +1,60 @@
1
+ import os
2
+ import struct
3
+ import fcntl
4
+ import termios
5
+ from django.db import connection
6
+ from django.conf import settings
7
+
8
+
9
+ def terminal_width():
10
+ """
11
+ Function to compute the terminal width.
12
+ """
13
+ width = 0
14
+ try:
15
+ s = struct.pack('HHHH', 0, 0, 0, 0)
16
+ x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
17
+ width = struct.unpack('HHHH', x)[1]
18
+ except (struct.error, fcntl.error, termios.error) as e:
19
+ print(f"Error computing terminal width: {e}")
20
+
21
+ if width <= 0:
22
+ try:
23
+ width = int(os.environ['COLUMNS'])
24
+ except (KeyError, ValueError) as e:
25
+ print(f"Error getting terminal width from environment: {e}")
26
+
27
+ return width if width > 0 else 80
28
+
29
+
30
+ def SqlPrintingMiddleware(get_response):
31
+ """
32
+ Middleware to print SQL queries in the terminal during a request in debug mode.
33
+ """
34
+ def middleware(request):
35
+ response = get_response(request)
36
+
37
+ if not settings.DEBUG or not connection.queries:
38
+ return response
39
+
40
+ indentation = 2
41
+ print(f"\n\n{' ' * indentation}\033[1;35m[SQL Queries for]\033[1;34m {request.path_info}\033[0m\n")
42
+ width = terminal_width()
43
+ total_time = 0.0
44
+
45
+ for query in connection.queries:
46
+ nice_sql = query['sql'].replace('"', '').replace(',', ', ')
47
+ sql = f"\033[1;31m[{query['time']}]\033[0m {nice_sql}"
48
+ total_time += float(query['time'])
49
+
50
+ while len(sql) > width - indentation:
51
+ print(f"{' ' * indentation}{sql[:width - indentation]}")
52
+ sql = sql[width - indentation:]
53
+ print(f"{' ' * indentation}{sql}\n")
54
+
55
+ print(f"{' ' * indentation}\033[1;32m[TOTAL TIME: {total_time} seconds]\033[0m")
56
+ print(f"{' ' * indentation}\033[1;32m[TOTAL QUERIES: {len(connection.queries)}]\033[0m")
57
+
58
+ return response
59
+
60
+ return middleware
@@ -0,0 +1,128 @@
1
+ import traceback
2
+ from typing import Callable, Awaitable, Type, Any
3
+ from starlette.middleware.base import BaseHTTPMiddleware
4
+ from fastapi.exceptions import RequestValidationError
5
+ from starlette.exceptions import HTTPException as StarletteHTTPException
6
+ from fastapi.responses import JSONResponse
7
+ from fk_utils.exceptions.custom_http_exception import CustomHTTPException
8
+ from fastapi import Request, Response
9
+ from fk_utils.middlewares.fastapi.i18n import _
10
+ import uuid
11
+ import logging
12
+ from enum import Enum
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ErrorCode(Enum):
18
+ VALIDATION_ERROR = (
19
+ "MAIN-VAL-001",
20
+ "Validation Error",
21
+ 422,
22
+ "The input data is invalid.",
23
+ )
24
+ RESOURCE_NOT_FOUND = (
25
+ "MAIN-VAL-002",
26
+ "Resource Not Found",
27
+ 404,
28
+ "The requested resource does not exist.",
29
+ )
30
+ SYSTEM_ERROR = (
31
+ "MAIN-SYS-001",
32
+ "System Error",
33
+ 500,
34
+ "An unexpected error occurred.",
35
+ )
36
+
37
+ def __init__(self, error_code: str, title: str, status_code: int, detail: str):
38
+ self.error_code = error_code
39
+ self.title = title
40
+ self.status_code = status_code
41
+ self.detail = detail
42
+
43
+
44
+ class ErrorMiddleware(BaseHTTPMiddleware):
45
+ def __init__(self, app: Callable, error_enum: Type[ErrorCode], **kwargs: Any) -> None:
46
+ """
47
+ Middleware to handle global errors in the application.
48
+
49
+ :param app: FastAPI application.
50
+ :param error_enum: Enumeration of error codes.
51
+ """
52
+ super().__init__(app, **kwargs)
53
+ self.error_enum = error_enum
54
+
55
+ async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
56
+ try:
57
+ response = await call_next(request)
58
+ return response
59
+ except CustomHTTPException as custom_exc:
60
+ return self.build_error_response(
61
+ error_code=custom_exc.error_code,
62
+ detail=custom_exc.detail,
63
+ request=request,
64
+ )
65
+ except RequestValidationError as validation_exc:
66
+ user_friendly_detail = self.format_validation_errors(validation_exc, request)
67
+ return self.build_error_response(
68
+ error_code=self.error_enum.VALIDATION_ERROR,
69
+ detail=user_friendly_detail,
70
+ request=request,
71
+ )
72
+ except StarletteHTTPException as http_exc:
73
+ error_code = (
74
+ self.error_enum.RESOURCE_NOT_FOUND
75
+ if http_exc.status_code == 404
76
+ else self.error_enum.VALIDATION_ERROR
77
+ )
78
+ return self.build_error_response(
79
+ error_code=error_code,
80
+ detail=str(http_exc.detail),
81
+ request=request,
82
+ )
83
+ except Exception:
84
+ return self.build_error_response(
85
+ error_code=self.error_enum.SYSTEM_ERROR,
86
+ detail="An unexpected error occurred.",
87
+ request=request,
88
+ )
89
+
90
+ def build_error_response(self, error_code: ErrorCode, detail: str, request: Request) -> JSONResponse:
91
+ """
92
+ Builds a JSON response for errors.
93
+
94
+ :param error_code: Error code.
95
+ :param detail: Error detail.
96
+ :param request: HTTP request.
97
+ :return: JSON response with the formatted error.
98
+ """
99
+ instance_id = str(uuid.uuid4())
100
+ translated_detail = _(detail, request) if isinstance(detail, str) else detail
101
+ logger.error(
102
+ f"Error: {error_code.error_code} - {error_code.title} - {translated_detail} - Instance: {instance_id} - Traceback: {traceback.format_exc()}"
103
+ )
104
+ return JSONResponse(
105
+ status_code=error_code.status_code,
106
+ content={
107
+ "error_code": error_code.error_code,
108
+ "title": _(error_code.title, request),
109
+ "status": error_code.status_code,
110
+ "detail": translated_detail,
111
+ "instance": instance_id,
112
+ },
113
+ )
114
+
115
+ def format_validation_errors(self, exc: RequestValidationError, request: Request) -> str:
116
+ error_messages = []
117
+ for error in exc.errors():
118
+ loc = "".join(map(str, error.get("loc", []))).replace("body", "")
119
+ msg = error.get("msg", "Invalid value")
120
+
121
+ ctx = error.get("ctx")
122
+ if ctx:
123
+ ctx_details = ", ".join(f"{value}" for key, value in ctx.items())
124
+ msg += f" {ctx_details}"
125
+
126
+ error_messages.append(f"{_(msg, request)}: {loc}")
127
+
128
+ return " | ".join(error_messages)
@@ -0,0 +1,66 @@
1
+ import os
2
+ import gettext
3
+ from pathlib import Path
4
+ import boto3
5
+ from botocore.exceptions import BotoCoreError, ClientError
6
+ from fastapi import Request
7
+ from mypy_boto3_translate.client import TranslateClient
8
+ from fk_utils.cache.redis import CacheService
9
+
10
+ AWS_REGION_NAME = os.environ.get("AWS_REGION_NAME", "us-east-2")
11
+
12
+
13
+ class TranslationWrapper:
14
+ def __init__(self, lang: str, translation_dir: Path, redis_url: str):
15
+ """Initializes the translation object for a specific language."""
16
+ self.lang = lang
17
+ self.translation_dir = translation_dir
18
+ self.cache_service = CacheService(redis_url)
19
+
20
+ self.translations = gettext.translation(
21
+ "messages",
22
+ localedir=self.translation_dir,
23
+ languages=[self.lang],
24
+ fallback=True,
25
+ )
26
+ self.translations.install()
27
+
28
+ self.translate_client: TranslateClient = boto3.client(
29
+ "translate", region_name=AWS_REGION_NAME
30
+ )
31
+
32
+ def gettext(self, message: str) -> str:
33
+ """Returns the translation from the file or AWS Translate."""
34
+ cache_key = f"translation:{self.lang}:{message}"
35
+ cached_translation = self.cache_service.get(cache_key)
36
+ if isinstance(cached_translation, dict) and self.lang in cached_translation:
37
+ return cached_translation[self.lang]
38
+
39
+ translated_message = self.translations.gettext(message)
40
+ if translated_message == message:
41
+ try:
42
+ response = self.translate_client.translate_text(
43
+ Text=message,
44
+ SourceLanguageCode="en",
45
+ TargetLanguageCode=self.lang,
46
+ )
47
+ translated_message = response["TranslatedText"]
48
+ self.cache_service.add(
49
+ cache_key, {self.lang: translated_message}, expiration_time=86400
50
+ )
51
+ except (BotoCoreError, ClientError) as e:
52
+ print(f"Error en AWS Translate: {e}")
53
+ translated_message = message
54
+
55
+ return translated_message
56
+
57
+
58
+ async def set_locale(request: Request, translation_dir: Path, redis_url: str):
59
+ """Sets the language according to the 'Accept-Language' header."""
60
+ lang = request.headers.get("Accept-Language", "en")
61
+ request.state.translation = TranslationWrapper(lang, translation_dir, redis_url)
62
+
63
+
64
+ def _(message: str, request: Request) -> str:
65
+ """Gets the translation of the message using the language configured in the request."""
66
+ return request.state.translation.gettext(message)
@@ -0,0 +1,21 @@
1
+ from typing import Awaitable, Callable
2
+ from fastapi import Request, Response
3
+ from starlette.middleware.base import BaseHTTPMiddleware
4
+ from pathlib import Path
5
+ from fk_utils.middlewares.fastapi.i18n import set_locale
6
+
7
+
8
+ class LanguageMiddleware(BaseHTTPMiddleware):
9
+ def __init__(self, app, *, translation_dir: Path, redis_url: str, **kwargs):
10
+ super().__init__(app, **kwargs)
11
+ self.translation_dir = translation_dir
12
+ self.redis_url = redis_url
13
+
14
+ async def dispatch(
15
+ self,
16
+ request: Request,
17
+ call_next: Callable[[Request], Awaitable[Response]],
18
+ ) -> Response:
19
+ await set_locale(request, self.translation_dir, self.redis_url)
20
+ response = await call_next(request)
21
+ return response
@@ -0,0 +1,70 @@
1
+ import os
2
+ import struct
3
+ import fcntl
4
+ import termios
5
+ from starlette.middleware.base import BaseHTTPMiddleware
6
+ from sqlalchemy import event
7
+ from sqlalchemy.engine import Engine
8
+
9
+
10
+ def terminal_width():
11
+ """
12
+ Function to compute the terminal width.
13
+ """
14
+ width = 0
15
+ try:
16
+ s = struct.pack('HHHH', 0, 0, 0, 0)
17
+ x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
18
+ width = struct.unpack('HHHH', x)[1]
19
+ except (struct.error, fcntl.error, termios.error) as e:
20
+ print(f"Error computing terminal width: {e}")
21
+
22
+ if width <= 0:
23
+ try:
24
+ width = int(os.environ['COLUMNS'])
25
+ except (KeyError, ValueError) as e:
26
+ print(f"Error getting terminal width from environment: {e}")
27
+
28
+ return width if width > 0 else 80
29
+
30
+
31
+ class SqlPrintingMiddleware(BaseHTTPMiddleware):
32
+ def __init__(self, app, debug: bool = True):
33
+ super().__init__(app)
34
+ self.debug = debug
35
+
36
+ async def dispatch(self, request, call_next):
37
+ if self.debug:
38
+ request.state.queries = []
39
+
40
+ @event.listens_for(Engine, "after_cursor_execute")
41
+ def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
42
+ if not hasattr(request.state, 'queries'):
43
+ request.state.queries = []
44
+ request.state.queries.append({
45
+ 'sql': statement,
46
+ 'time': context.execution_time
47
+ })
48
+
49
+ response = await call_next(request)
50
+
51
+ if self.debug and hasattr(request.state, 'queries'):
52
+ indentation = 2
53
+ print(f"\n\n{' ' * indentation}\033[1;35m[SQL Queries for]\033[1;34m {request.url.path}\033[0m\n")
54
+ width = terminal_width()
55
+ total_time = 0.0
56
+
57
+ for query in request.state.queries:
58
+ nice_sql = query['sql'].replace('"', '').replace(',', ', ')
59
+ sql = f"\033[1;31m[{query['time']}]\033[0m {nice_sql}"
60
+ total_time += float(query['time'])
61
+
62
+ while len(sql) > width - indentation:
63
+ print(f"{' ' * indentation}{sql[:width - indentation]}")
64
+ sql = sql[width - indentation:]
65
+ print(f"{' ' * indentation}{sql}\n")
66
+
67
+ print(f"{' ' * indentation}\033[1;32m[TOTAL TIME: {total_time} seconds]\033[0m")
68
+ print(f"{' ' * indentation}\033[1;32m[TOTAL QUERIES: {len(request.state.queries)}]\033[0m")
69
+
70
+ return response
@@ -0,0 +1,66 @@
1
+ import os
2
+ import struct
3
+ import fcntl
4
+ import termios
5
+ from flask import request, g
6
+ from sqlalchemy import event
7
+ from sqlalchemy.engine import Engine
8
+
9
+
10
+ def terminal_width():
11
+ """
12
+ Function to compute the terminal width.
13
+ """
14
+ width = 0
15
+ try:
16
+ s = struct.pack('HHHH', 0, 0, 0, 0)
17
+ x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
18
+ width = struct.unpack('HHHH', x)[1]
19
+ except (struct.error, fcntl.error, termios.error) as e:
20
+ print(f"Error computing terminal width: {e}")
21
+
22
+ if width <= 0:
23
+ try:
24
+ width = int(os.environ['COLUMNS'])
25
+ except (KeyError, ValueError) as e:
26
+ print(f"Error getting terminal width from environment: {e}")
27
+
28
+ return width if width > 0 else 80
29
+
30
+
31
+ def SqlPrintingMiddleware(app):
32
+ @app.before_request
33
+ def before_request():
34
+ g.queries = []
35
+
36
+ @event.listens_for(Engine, "after_cursor_execute")
37
+ def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
38
+ if 'queries' not in g:
39
+ g.queries = []
40
+ g.queries.append({
41
+ 'sql': statement,
42
+ 'time': context.execution_time
43
+ })
44
+
45
+ @app.after_request
46
+ def after_request(response):
47
+ if app.config['DEBUG'] and hasattr(g, 'queries'):
48
+ indentation = 2
49
+ print(f"\n\n{' ' * indentation}\033[1;35m[SQL Queries for]\033[1;34m {request.path}\033[0m\n")
50
+ width = terminal_width()
51
+ total_time = 0.0
52
+
53
+ for query in g.queries:
54
+ nice_sql = query['sql'].replace('"', '').replace(',', ', ')
55
+ sql = f"\033[1;31m[{query['time']}]\033[0m {nice_sql}"
56
+ total_time += float(query['time'])
57
+
58
+ while len(sql) > width - indentation:
59
+ print(f"{' ' * indentation}{sql[:width - indentation]}")
60
+ sql = sql[width - indentation:]
61
+ print(f"{' ' * indentation}{sql}\n")
62
+
63
+ print(f"{' ' * indentation}\033[1;32m[TOTAL TIME: {total_time} seconds]\033[0m")
64
+ print(f"{' ' * indentation}\033[1;32m[TOTAL QUERIES: {len(g.queries)}]\033[0m")
65
+
66
+ return response
File without changes
File without changes
@@ -0,0 +1,68 @@
1
+ # opentelemetry_config.py
2
+ import logging
3
+ from opentelemetry import trace
4
+ from opentelemetry.sdk.trace import TracerProvider
5
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
6
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
7
+ from opentelemetry.sdk.resources import Resource
8
+ from opentelemetry.instrumentation.django import DjangoInstrumentor
9
+ from opentelemetry.instrumentation.requests import RequestsInstrumentor
10
+ from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
11
+
12
+ from django.apps import AppConfig
13
+
14
+ from fk_utils import SETTINGS
15
+
16
+ # Configurar logging
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def configure_tracer():
22
+ try:
23
+ # Configurar el proveedor de trazas
24
+ trace.set_tracer_provider(TracerProvider(
25
+ resource=Resource.create({"service.name": SETTINGS.OTLP_NAME})
26
+ ))
27
+
28
+ # Configurar el exportador OTLP
29
+ otlp_exporter = OTLPSpanExporter(
30
+ endpoint=f"{SETTINGS.OTLP_HOST}:{SETTINGS.OTLP_PORT}",
31
+ insecure=True
32
+ )
33
+
34
+ # Añadir el procesador de spans
35
+ trace.get_tracer_provider().add_span_processor(
36
+ BatchSpanProcessor(otlp_exporter)
37
+ )
38
+
39
+ logger.info("OpenTelemetry tracer configurado correctamente.")
40
+
41
+ except Exception as e:
42
+ logger.error(f"Error al configurar el tracer de OpenTelemetry: {e}")
43
+
44
+
45
+ def instrument_app():
46
+ try:
47
+ # Instrumentar Django
48
+ DjangoInstrumentor().instrument()
49
+
50
+ # Instrumentar Requests
51
+ RequestsInstrumentor().instrument()
52
+
53
+ # Instrumentar PostgreSQL
54
+ Psycopg2Instrumentor().instrument()
55
+
56
+ logger.info("Aplicación instrumentada correctamente con OpenTelemetry.")
57
+
58
+ except Exception as e:
59
+ logger.error(f"Error al instrumentar la aplicación: {e}")
60
+
61
+
62
+ class OpenTelemetryConfig(AppConfig):
63
+ name = 'fk_utils.traces.opentelemetry.django.trace'
64
+ verbose_name = "OpenTelemetry Integration"
65
+
66
+ def ready(self):
67
+ configure_tracer()
68
+ instrument_app()
@@ -0,0 +1,56 @@
1
+ import logging
2
+ from opentelemetry import trace
3
+ from opentelemetry.sdk.trace import TracerProvider
4
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
5
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
6
+ from opentelemetry.sdk.resources import Resource
7
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
8
+ from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
9
+ from opentelemetry.instrumentation.requests import RequestsInstrumentor
10
+
11
+ from fk_utils import SETTINGS
12
+
13
+ # Configurar logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def configure_tracer():
19
+ try:
20
+ # Configurar el proveedor de trazas
21
+ trace.set_tracer_provider(TracerProvider(
22
+ resource=Resource.create({"service.name": SETTINGS.OTLP_NAME})
23
+ ))
24
+
25
+ # Configurar el exportador OTLP
26
+ otlp_exporter = OTLPSpanExporter(
27
+ endpoint=f"{SETTINGS.OTLP_HOST}:{SETTINGS.OTLP_PORT}",
28
+ insecure=True
29
+ )
30
+
31
+ # Añadir el procesador de spans
32
+ trace.get_tracer_provider().add_span_processor(
33
+ BatchSpanProcessor(otlp_exporter)
34
+ )
35
+
36
+ logger.info("OpenTelemetry tracer configurado correctamente.")
37
+
38
+ except Exception as e:
39
+ logger.error(f"Error al configurar el tracer de OpenTelemetry: {e}")
40
+
41
+
42
+ def instrument_app(app):
43
+ try:
44
+ # Instrumentar FastAPI
45
+ FastAPIInstrumentor.instrument_app(app)
46
+
47
+ # Instrumentar SQLAlchemy
48
+ SQLAlchemyInstrumentor().instrument()
49
+
50
+ # Instrumentar Requests
51
+ RequestsInstrumentor().instrument()
52
+
53
+ logger.info("Aplicación instrumentada correctamente con OpenTelemetry.")
54
+
55
+ except Exception as e:
56
+ logger.error(f"Error al instrumentar la aplicación: {e}")
@@ -0,0 +1,59 @@
1
+ import logging
2
+ from opentelemetry import trace
3
+ from opentelemetry.sdk.trace import TracerProvider
4
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
5
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
6
+ from opentelemetry.sdk.resources import Resource
7
+ from opentelemetry.instrumentation.flask import FlaskInstrumentor
8
+ from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
9
+ from opentelemetry.instrumentation.requests import RequestsInstrumentor
10
+
11
+ from fk_utils import SETTINGS
12
+
13
+ # Configurar logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def configure_tracer():
19
+ try:
20
+ # Configurar el proveedor de trazas
21
+ trace.set_tracer_provider(TracerProvider(
22
+ resource=Resource.create({"service.name": SETTINGS.OTLP_NAME})
23
+ ))
24
+
25
+ # Configurar el exportador OTLP
26
+ otlp_exporter = OTLPSpanExporter(
27
+ endpoint=f"{SETTINGS.OTLP_HOST}:{SETTINGS.OTLP_PORT}",
28
+ insecure=True
29
+ )
30
+
31
+ # Añadir el procesador de spans
32
+ trace.get_tracer_provider().add_span_processor(
33
+ BatchSpanProcessor(otlp_exporter)
34
+ )
35
+
36
+ logger.info("OpenTelemetry tracer configurado correctamente.")
37
+
38
+ except Exception as e:
39
+ logger.error(f"Error al configurar el tracer de OpenTelemetry: {e}")
40
+
41
+
42
+ def instrument_app(app):
43
+ try:
44
+ # Configurar el tracer
45
+ configure_tracer()
46
+
47
+ # Instrumentar Flask
48
+ FlaskInstrumentor().instrument_app(app)
49
+
50
+ # Instrumentar SQLAlchemy
51
+ SQLAlchemyInstrumentor().instrument()
52
+
53
+ # Instrumentar Requests
54
+ RequestsInstrumentor().instrument()
55
+
56
+ logger.info("Aplicación instrumentada correctamente con OpenTelemetry.")
57
+
58
+ except Exception as e:
59
+ logger.error(f"Error al instrumentar la aplicación: {e}")
@@ -0,0 +1,28 @@
1
+ [tool.poetry]
2
+ name = "fk_util_tools"
3
+ version = "0.1.14"
4
+ description = "Herramientas de utilidad o funciones comunes para los proyectos de FK."
5
+ authors = ["Steven Santacruz Garcia <stevengarcia1118@gmail.com>"]
6
+ license = "MIT"
7
+ homepage = "https://gitlab.com/f3315/team-back-end/cross/packages/package_utils"
8
+ packages = [{include = "fk_utils"}]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = ">=3.7,<4.0"
12
+ boto3 = "*"
13
+ opentelemetry-api = "*"
14
+ opentelemetry-sdk = "*"
15
+ opentelemetry-instrumentation-fastapi = "*"
16
+ opentelemetry-instrumentation-flask = "*"
17
+ opentelemetry-instrumentation-django = "*"
18
+ opentelemetry-exporter-otlp = "*"
19
+ opentelemetry-instrumentation-sqlalchemy = "*"
20
+ opentelemetry-instrumentation-requests = "*"
21
+ opentelemetry-exporter-jaeger = "*"
22
+ opentelemetry-instrumentation-psycopg2 = "*"
23
+ psycopg2-binary = "*"
24
+ starlette = "*"
25
+
26
+ [build-system]
27
+ requires = ["poetry-core>=1.0.0"]
28
+ build-backend = "poetry.core.masonry.api"