ai4i-core 1.0.0__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 (64) hide show
  1. ai4i_core-1.0.0/LICENSE +21 -0
  2. ai4i_core-1.0.0/PKG-INFO +109 -0
  3. ai4i_core-1.0.0/README.md +50 -0
  4. ai4i_core-1.0.0/ai4i_core/__init__.py +13 -0
  5. ai4i_core-1.0.0/ai4i_core/bootstrap/__init__.py +32 -0
  6. ai4i_core-1.0.0/ai4i_core/bootstrap/cache.py +31 -0
  7. ai4i_core-1.0.0/ai4i_core/bootstrap/database.py +75 -0
  8. ai4i_core-1.0.0/ai4i_core/bootstrap/factory.py +114 -0
  9. ai4i_core-1.0.0/ai4i_core/bootstrap/health.py +64 -0
  10. ai4i_core-1.0.0/ai4i_core/bootstrap/rate_limit.py +38 -0
  11. ai4i_core-1.0.0/ai4i_core/bootstrap/redis.py +66 -0
  12. ai4i_core-1.0.0/ai4i_core/bootstrap/schemas.py +15 -0
  13. ai4i_core-1.0.0/ai4i_core/bootstrap/versioning.py +119 -0
  14. ai4i_core-1.0.0/ai4i_core/context.py +67 -0
  15. ai4i_core-1.0.0/ai4i_core/email/__init__.py +18 -0
  16. ai4i_core-1.0.0/ai4i_core/email/client.py +52 -0
  17. ai4i_core-1.0.0/ai4i_core/email/exceptions.py +10 -0
  18. ai4i_core-1.0.0/ai4i_core/email/fastapi.py +34 -0
  19. ai4i_core-1.0.0/ai4i_core/email/message.py +87 -0
  20. ai4i_core-1.0.0/ai4i_core/email/providers/__init__.py +11 -0
  21. ai4i_core-1.0.0/ai4i_core/email/providers/base.py +21 -0
  22. ai4i_core-1.0.0/ai4i_core/email/providers/console.py +50 -0
  23. ai4i_core-1.0.0/ai4i_core/email/providers/factory.py +65 -0
  24. ai4i_core-1.0.0/ai4i_core/email/providers/smtp.py +72 -0
  25. ai4i_core-1.0.0/ai4i_core/email/settings.py +77 -0
  26. ai4i_core-1.0.0/ai4i_core/email/templates.py +37 -0
  27. ai4i_core-1.0.0/ai4i_core/exceptions/__init__.py +114 -0
  28. ai4i_core-1.0.0/ai4i_core/exceptions/exceptions.py +329 -0
  29. ai4i_core-1.0.0/ai4i_core/exceptions/handlers.py +401 -0
  30. ai4i_core-1.0.0/ai4i_core/exceptions/responses.py +26 -0
  31. ai4i_core-1.0.0/ai4i_core/logging/__init__.py +46 -0
  32. ai4i_core-1.0.0/ai4i_core/logging/config.py +56 -0
  33. ai4i_core-1.0.0/ai4i_core/logging/context.py +27 -0
  34. ai4i_core-1.0.0/ai4i_core/logging/formatters.py +101 -0
  35. ai4i_core-1.0.0/ai4i_core/logging/logger.py +119 -0
  36. ai4i_core-1.0.0/ai4i_core/logging/middleware.py +117 -0
  37. ai4i_core-1.0.0/ai4i_core/observability/__init__.py +22 -0
  38. ai4i_core-1.0.0/ai4i_core/observability/config.py +22 -0
  39. ai4i_core-1.0.0/ai4i_core/observability/metrics.py +312 -0
  40. ai4i_core-1.0.0/ai4i_core/observability/middleware.py +543 -0
  41. ai4i_core-1.0.0/ai4i_core/observability/plugin.py +52 -0
  42. ai4i_core-1.0.0/ai4i_core/telemetry/__init__.py +63 -0
  43. ai4i_core-1.0.0/ai4i_core/telemetry/propagator.py +95 -0
  44. ai4i_core-1.0.0/ai4i_core/telemetry/registry.py +411 -0
  45. ai4i_core-1.0.0/ai4i_core/telemetry/trace_middleware.py +22 -0
  46. ai4i_core-1.0.0/ai4i_core/telemetry/trace_wrapper.py +114 -0
  47. ai4i_core-1.0.0/ai4i_core/telemetry/traceability.py +277 -0
  48. ai4i_core-1.0.0/ai4i_core/telemetry/util/asr/stages.json +43 -0
  49. ai4i_core-1.0.0/ai4i_core/telemetry/util/audio_language_detection/stages.json +43 -0
  50. ai4i_core-1.0.0/ai4i_core/telemetry/util/language_detection/stages.json +43 -0
  51. ai4i_core-1.0.0/ai4i_core/telemetry/util/language_diarization/stages.json +43 -0
  52. ai4i_core-1.0.0/ai4i_core/telemetry/util/ner/stages.json +43 -0
  53. ai4i_core-1.0.0/ai4i_core/telemetry/util/nmt/stages.json +43 -0
  54. ai4i_core-1.0.0/ai4i_core/telemetry/util/ocr/stages.json +43 -0
  55. ai4i_core-1.0.0/ai4i_core/telemetry/util/speakerdiarization/stages.json +43 -0
  56. ai4i_core-1.0.0/ai4i_core/telemetry/util/transliteration/stages.json +43 -0
  57. ai4i_core-1.0.0/ai4i_core/telemetry/util/tts/stages.json +43 -0
  58. ai4i_core-1.0.0/ai4i_core.egg-info/PKG-INFO +109 -0
  59. ai4i_core-1.0.0/ai4i_core.egg-info/SOURCES.txt +62 -0
  60. ai4i_core-1.0.0/ai4i_core.egg-info/dependency_links.txt +1 -0
  61. ai4i_core-1.0.0/ai4i_core.egg-info/requires.txt +32 -0
  62. ai4i_core-1.0.0/ai4i_core.egg-info/top_level.txt +1 -0
  63. ai4i_core-1.0.0/pyproject.toml +100 -0
  64. ai4i_core-1.0.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI4I Team
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,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai4i-core
3
+ Version: 1.0.0
4
+ Summary: AI4I Core - consolidated utility libraries (exceptions, logging, telemetry, observability, bootstrap, email) for AI4I microservices
5
+ Author-email: AI4Inclusion <support@ai4inclusion.org>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/COSS-India/ai4i-core
8
+ Project-URL: Repository, https://github.com/COSS-India/ai4i-core
9
+ Project-URL: Issues, https://github.com/COSS-India/ai4i-core/issues
10
+ Project-URL: Source, https://github.com/COSS-India/ai4i-core/tree/master/libs/ai4i_core
11
+ Keywords: ai4i,fastapi,logging,telemetry,observability,bootstrap,email,exceptions
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Framework :: FastAPI
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: System :: Logging
23
+ Classifier: Topic :: System :: Monitoring
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: fastapi>=0.104.0
28
+ Requires-Dist: starlette>=0.27.0
29
+ Requires-Dist: pydantic>=2.5.0
30
+ Requires-Dist: pydantic-settings>=2.0.0
31
+ Requires-Dist: python-dotenv>=1.0.0
32
+ Requires-Dist: httpx>=0.25.0
33
+ Requires-Dist: redis>=5.0.0
34
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.0
35
+ Requires-Dist: slowapi>=0.1.9
36
+ Requires-Dist: aiosmtplib<4.0.0,>=3.0.0
37
+ Requires-Dist: jinja2<4.0.0,>=3.1.0
38
+ Requires-Dist: python-json-logger>=2.0.7
39
+ Requires-Dist: kafka-python>=2.0.2
40
+ Requires-Dist: aiokafka>=0.8.0
41
+ Requires-Dist: prometheus-client>=0.19.0
42
+ Requires-Dist: psutil>=5.9.0
43
+ Requires-Dist: PyJWT>=2.8.0
44
+ Requires-Dist: tritonclient[http]>=2.40.0
45
+ Requires-Dist: numpy>=1.24.0
46
+ Requires-Dist: packaging>=21.0
47
+ Requires-Dist: opentelemetry-api>=1.20.0
48
+ Requires-Dist: opentelemetry-sdk>=1.20.0
49
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.41b0
50
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20.0
51
+ Provides-Extra: dev
52
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
53
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
54
+ Requires-Dist: black>=23.0.0; extra == "dev"
55
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
56
+ Requires-Dist: build>=1.0.0; extra == "dev"
57
+ Requires-Dist: twine>=4.0.0; extra == "dev"
58
+ Dynamic: license-file
59
+
60
+ # ai4i-core
61
+
62
+ Consolidated utility libraries for AI4ICore microservices. A single installable package that bundles what used to live across ten standalone libraries — constants, env, exceptions, logging, telemetry, observability, model management, service base, bootstrap, and email.
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install ai4i-core
68
+ ```
69
+
70
+ ## Subpackages
71
+
72
+ | Subpackage | Purpose |
73
+ | --- | --- |
74
+ | `ai4i_core.bootstrap` | FastAPI app bootstrap helpers (cache, database, redis, schemas, versioning) |
75
+ | `ai4i_core.constants` | Static error codes, messages, and back-compat re-exports |
76
+ | `ai4i_core.email` | Provider-agnostic transactional email client (SMTP, console, pluggable) |
77
+ | `ai4i_core.env` | Pydantic-based environment / settings |
78
+ | `ai4i_core.exceptions` | Shared exception hierarchy, response envelope, FastAPI handlers |
79
+ | `ai4i_core.logging` | Structured JSON logging with trace correlation |
80
+ | `ai4i_core.model_management` | Model management client, Triton inference, FastAPI middleware |
81
+ | `ai4i_core.observability` | Prometheus metrics, dashboards, middleware |
82
+ | `ai4i_core.service_base` | App factory, health, rate limit, service registry, inference headers |
83
+ | `ai4i_core.telemetry` | OpenTelemetry tracing, OpenSearch query clients |
84
+
85
+ ## Usage
86
+
87
+ ```python
88
+ from ai4i_core.env import app_env
89
+ from ai4i_core.logging import get_logger, register_logging_plugin
90
+ from ai4i_core.exceptions import register_exception_handlers
91
+ from ai4i_core.observability import ObservabilityPlugin, PluginConfig
92
+ from ai4i_core.telemetry import register_telemetry_plugin, TelemetryConfig
93
+ from ai4i_core.service_base import create_inference_app
94
+ ```
95
+
96
+ See each subpackage's source for the full surface.
97
+
98
+ ## Requirements
99
+
100
+ - Python `>= 3.11`
101
+
102
+ ## License
103
+
104
+ MIT — see [LICENSE](LICENSE).
105
+
106
+ ## Links
107
+
108
+ - Source: <https://github.com/COSS-India/ai4i-core>
109
+ - Issues: <https://github.com/COSS-India/ai4i-core/issues>
@@ -0,0 +1,50 @@
1
+ # ai4i-core
2
+
3
+ Consolidated utility libraries for AI4ICore microservices. A single installable package that bundles what used to live across ten standalone libraries — constants, env, exceptions, logging, telemetry, observability, model management, service base, bootstrap, and email.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install ai4i-core
9
+ ```
10
+
11
+ ## Subpackages
12
+
13
+ | Subpackage | Purpose |
14
+ | --- | --- |
15
+ | `ai4i_core.bootstrap` | FastAPI app bootstrap helpers (cache, database, redis, schemas, versioning) |
16
+ | `ai4i_core.constants` | Static error codes, messages, and back-compat re-exports |
17
+ | `ai4i_core.email` | Provider-agnostic transactional email client (SMTP, console, pluggable) |
18
+ | `ai4i_core.env` | Pydantic-based environment / settings |
19
+ | `ai4i_core.exceptions` | Shared exception hierarchy, response envelope, FastAPI handlers |
20
+ | `ai4i_core.logging` | Structured JSON logging with trace correlation |
21
+ | `ai4i_core.model_management` | Model management client, Triton inference, FastAPI middleware |
22
+ | `ai4i_core.observability` | Prometheus metrics, dashboards, middleware |
23
+ | `ai4i_core.service_base` | App factory, health, rate limit, service registry, inference headers |
24
+ | `ai4i_core.telemetry` | OpenTelemetry tracing, OpenSearch query clients |
25
+
26
+ ## Usage
27
+
28
+ ```python
29
+ from ai4i_core.env import app_env
30
+ from ai4i_core.logging import get_logger, register_logging_plugin
31
+ from ai4i_core.exceptions import register_exception_handlers
32
+ from ai4i_core.observability import ObservabilityPlugin, PluginConfig
33
+ from ai4i_core.telemetry import register_telemetry_plugin, TelemetryConfig
34
+ from ai4i_core.service_base import create_inference_app
35
+ ```
36
+
37
+ See each subpackage's source for the full surface.
38
+
39
+ ## Requirements
40
+
41
+ - Python `>= 3.11`
42
+
43
+ ## License
44
+
45
+ MIT — see [LICENSE](LICENSE).
46
+
47
+ ## Links
48
+
49
+ - Source: <https://github.com/COSS-India/ai4i-core>
50
+ - Issues: <https://github.com/COSS-India/ai4i-core/issues>
@@ -0,0 +1,13 @@
1
+ """
2
+ ai4i_core — Consolidated AI4I utility libraries.
3
+
4
+ Subpackages:
5
+ bootstrap — FastAPI app bootstrap helpers (cache, db, redis, schemas, versioning)
6
+ email — Provider-agnostic transactional email client (SMTP, console, ...)
7
+ exceptions — Shared exception hierarchy, response envelope, FastAPI handlers
8
+ logging — Structured JSON logging with trace correlation
9
+ observability — Prometheus metrics, middleware
10
+ telemetry — OpenTelemetry tracing, OpenSearch query clients
11
+ """
12
+
13
+ __version__ = "1.1.5"
@@ -0,0 +1,32 @@
1
+ """
2
+ ai4icore_bootstrap — Infrastructure building blocks for ALL AI4I-Core microservices.
3
+
4
+ Provides:
5
+ - create_service_app() — single-call app factory
6
+ - Database: init_database, close_database, get_db
7
+ - Redis: init_redis, close_redis, get_redis
8
+ - Rate limiting: setup_rate_limiting, limiter
9
+ - Health: create_health_router
10
+ - Caching: CacheService
11
+ - Schemas: BaseSchema
12
+ """
13
+
14
+ from .factory import create_service_app, ServiceConfig
15
+ from .database import init_database, close_database, get_db, get_engine
16
+ from .redis import init_redis, close_redis, get_redis, get_redis_client
17
+ from .rate_limit import setup_rate_limiting, limiter
18
+ from .health import create_health_router
19
+ from .cache import CacheService
20
+ from .schemas import BaseSchema
21
+ from .versioning import APIVersioning, VersionInfo
22
+
23
+ __all__ = [
24
+ "create_service_app", "ServiceConfig",
25
+ "init_database", "close_database", "get_db", "get_engine",
26
+ "init_redis", "close_redis", "get_redis", "get_redis_client",
27
+ "setup_rate_limiting", "limiter",
28
+ "create_health_router",
29
+ "CacheService",
30
+ "BaseSchema",
31
+ "APIVersioning", "VersionInfo",
32
+ ]
@@ -0,0 +1,31 @@
1
+ """
2
+ Shared Redis caching patterns for microservices.
3
+
4
+ Provides generic key-value helpers on a single Redis connection (logical DB 0).
5
+ """
6
+
7
+ import logging
8
+ from typing import Optional
9
+
10
+ import redis.asyncio as aioredis
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class CacheService:
16
+ """Generic Redis caching operations on one client."""
17
+
18
+ def __init__(self, redis_client: aioredis.Redis) -> None:
19
+ self._redis = redis_client
20
+
21
+ async def set(self, key: str, value: str, ttl: int) -> None:
22
+ await self._redis.setex(key, ttl, value)
23
+
24
+ async def get(self, key: str) -> Optional[str]:
25
+ return await self._redis.get(key)
26
+
27
+ async def delete(self, key: str) -> None:
28
+ await self._redis.delete(key)
29
+
30
+ async def exists(self, key: str) -> bool:
31
+ return await self._redis.exists(key) > 0
@@ -0,0 +1,75 @@
1
+ """
2
+ Async SQLAlchemy engine, session factory, and get_db dependency.
3
+
4
+ Used by ALL microservices. No service-specific imports.
5
+ """
6
+
7
+ import logging
8
+ from collections.abc import AsyncGenerator
9
+
10
+ from sqlalchemy.ext.asyncio import (
11
+ AsyncEngine,
12
+ AsyncSession,
13
+ async_sessionmaker,
14
+ create_async_engine,
15
+ )
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ _engine: AsyncEngine | None = None
20
+ _session_factory: async_sessionmaker[AsyncSession] | None = None
21
+
22
+
23
+ def init_database(
24
+ db_url: str,
25
+ pool_size: int = 20,
26
+ max_overflow: int = 10,
27
+ echo: bool = False,
28
+ ) -> None:
29
+ """Create the async engine and session factory. Called during app startup."""
30
+ global _engine, _session_factory
31
+
32
+ logger.info("Connecting to database: %s", db_url.split("@")[-1])
33
+
34
+ _engine = create_async_engine(
35
+ db_url,
36
+ pool_size=pool_size,
37
+ max_overflow=max_overflow,
38
+ pool_pre_ping=True,
39
+ echo=echo,
40
+ )
41
+ _session_factory = async_sessionmaker(
42
+ bind=_engine,
43
+ class_=AsyncSession,
44
+ expire_on_commit=False,
45
+ )
46
+ logger.info("Database engine initialized.")
47
+
48
+
49
+ async def close_database() -> None:
50
+ """Dispose of the engine. Called during app shutdown."""
51
+ global _engine, _session_factory
52
+ if _engine:
53
+ await _engine.dispose()
54
+ logger.info("Database engine disposed.")
55
+ _engine = None
56
+ _session_factory = None
57
+
58
+
59
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
60
+ """FastAPI dependency that yields an async DB session."""
61
+ if _session_factory is None:
62
+ raise RuntimeError("Database not initialized. Call init_database() first.")
63
+ async with _session_factory() as session:
64
+ try:
65
+ yield session
66
+ except Exception:
67
+ await session.rollback()
68
+ raise
69
+
70
+
71
+ def get_engine() -> AsyncEngine:
72
+ """Return the current engine (for Alembic / telemetry instrumentation)."""
73
+ if _engine is None:
74
+ raise RuntimeError("Database not initialized.")
75
+ return _engine
@@ -0,0 +1,114 @@
1
+ """
2
+ Service app factory — the single way to create a FastAPI app in AI4I-Core.
3
+
4
+ Middleware execution order (LIFO — last added runs first on request):
5
+
6
+ CORSMiddleware ← outermost: applies CORS headers before anything else
7
+ RequestMiddleware ← seeds trace_id, logs the request
8
+ OTel / FastAPIInstrumentor ← CorrelationPropagator reads trace_id here
9
+
10
+ CORSMiddleware MUST be added last so it is outermost and CORS headers are
11
+ applied even when inner middleware short-circuits the request.
12
+ RequestMiddleware is added before CORSMiddleware so it still runs before OTel.
13
+ """
14
+
15
+ import logging
16
+ from dataclasses import dataclass, field
17
+ from typing import Optional
18
+
19
+ from fastapi import FastAPI
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ @dataclass
26
+ class ServiceConfig:
27
+ service_name: str
28
+ version: str = "1.0.0"
29
+ description: str = ""
30
+ environment: str = "development"
31
+
32
+ cors_origins: list[str] = field(default_factory=lambda: ["*"])
33
+ hide_docs_in_production: bool = True
34
+
35
+ telemetry_enabled: bool = True
36
+ jaeger_endpoint: Optional[str] = None
37
+
38
+ log_level: str = "INFO"
39
+
40
+ telemetry_exclude_paths: set[str] = field(default_factory=lambda: {
41
+ "/health", "/ready", "/docs", "/redoc", "/openapi.json",
42
+ })
43
+
44
+
45
+ def create_service_app(config: Optional[ServiceConfig] = None, **kwargs) -> FastAPI:
46
+ """Create a fully bootstrapped FastAPI application."""
47
+ if config is None:
48
+ config = ServiceConfig(**kwargs)
49
+
50
+ is_prod = config.environment in ("production", "staging")
51
+
52
+ app = FastAPI(
53
+ title=config.service_name,
54
+ version=config.version,
55
+ description=config.description or f"{config.service_name} microservice",
56
+ docs_url=None if (is_prod and config.hide_docs_in_production) else "/docs",
57
+ redoc_url=None if (is_prod and config.hide_docs_in_production) else "/redoc",
58
+ openapi_url=None if (is_prod and config.hide_docs_in_production) else "/openapi.json",
59
+ )
60
+
61
+ # ── 1. Exception handlers ──
62
+ try:
63
+ from ai4i_core.exceptions import register_exception_handlers
64
+ register_exception_handlers(app)
65
+ except ImportError:
66
+ pass
67
+
68
+ # ── 2. Configure logging ──
69
+ from ai4i_core.logging import configure_logging
70
+ configure_logging(service_name=config.service_name, log_level=config.log_level)
71
+
72
+ # ── 3. OTel instrumentation ──
73
+ if config.telemetry_enabled:
74
+ try:
75
+ from ai4i_core.telemetry import setup_tracing
76
+ setup_tracing(config.service_name, config.jaeger_endpoint)
77
+ except ImportError:
78
+ pass
79
+
80
+ try:
81
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
82
+ FastAPIInstrumentor.instrument_app(
83
+ app,
84
+ excluded_urls=",".join(config.telemetry_exclude_paths),
85
+ )
86
+ except ImportError:
87
+ pass
88
+
89
+ # ── 4. Request middleware (seeds trace_id before OTel propagator runs) ──
90
+ from ai4i_core.logging import RequestMiddleware
91
+ app.add_middleware(RequestMiddleware)
92
+
93
+ # ── 5. CORS (outermost — added last, runs first) ──
94
+ allow_all = config.cors_origins == ["*"]
95
+ app.add_middleware(
96
+ CORSMiddleware,
97
+ allow_origins=config.cors_origins,
98
+ allow_credentials=not allow_all,
99
+ allow_methods=["*"],
100
+ allow_headers=["*"],
101
+ )
102
+
103
+ # ── 6. Health endpoints ──
104
+ @app.get("/")
105
+ async def _root():
106
+ return {"service": config.service_name, "version": config.version, "status": "running"}
107
+
108
+ @app.get("/health")
109
+ async def _health():
110
+ return {"status": "healthy"}
111
+
112
+ app.state.service_config = config
113
+ logger.info("[bootstrap] %s v%s ready [%s]", config.service_name, config.version, config.environment)
114
+ return app
@@ -0,0 +1,64 @@
1
+ """
2
+ Health and readiness endpoints.
3
+
4
+ Used by ALL microservices. Returns 503 on probe failure (K8s compatible).
5
+ """
6
+
7
+ import logging
8
+
9
+ from fastapi import APIRouter, Depends
10
+ from fastapi.responses import JSONResponse
11
+ from sqlalchemy import text
12
+ from sqlalchemy.ext.asyncio import AsyncSession
13
+
14
+ import redis.asyncio as aioredis
15
+
16
+ from .database import get_db
17
+ from .redis import get_redis
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def create_health_router(service_name: str = "service", version: str = "1.0.0") -> APIRouter:
23
+ """
24
+ Create a health router with /, /health, /ready endpoints.
25
+ The /ready endpoint checks DB and Redis connectivity.
26
+ """
27
+ router = APIRouter()
28
+
29
+ @router.get("/")
30
+ async def root():
31
+ return {"service": service_name, "version": version, "status": "running"}
32
+
33
+ @router.get("/health")
34
+ async def health():
35
+ return {"status": "healthy"}
36
+
37
+ @router.get("/ready")
38
+ async def readiness(
39
+ db: AsyncSession = Depends(get_db),
40
+ redis_client: aioredis.Redis = Depends(get_redis),
41
+ ):
42
+ checks: dict[str, str] = {}
43
+
44
+ try:
45
+ await db.execute(text("SELECT 1"))
46
+ checks["database"] = "ok"
47
+ except Exception as exc:
48
+ logger.warning("Readiness: database check failed: %s", exc)
49
+ checks["database"] = "unavailable"
50
+
51
+ try:
52
+ await redis_client.ping()
53
+ checks["redis"] = "ok"
54
+ except Exception as exc:
55
+ logger.warning("Readiness: redis check failed: %s", exc)
56
+ checks["redis"] = "unavailable"
57
+
58
+ all_ok = all(v == "ok" for v in checks.values())
59
+ return JSONResponse(
60
+ status_code=200 if all_ok else 503,
61
+ content={"success": True, "data": {"ready": all_ok, "checks": checks}},
62
+ )
63
+
64
+ return router
@@ -0,0 +1,38 @@
1
+ """
2
+ Rate limiting using slowapi.
3
+
4
+ Used by ALL microservices. Defense-in-depth alongside APISIX.
5
+ """
6
+
7
+ from slowapi import Limiter
8
+ from slowapi.util import get_remote_address
9
+ from slowapi.errors import RateLimitExceeded
10
+ from slowapi.middleware import SlowAPIMiddleware
11
+
12
+ from fastapi import FastAPI, Request
13
+ from fastapi.responses import JSONResponse
14
+
15
+ limiter = Limiter(
16
+ key_func=get_remote_address,
17
+ default_limits=["200/minute"],
18
+ storage_uri="memory://",
19
+ )
20
+
21
+
22
+ def setup_rate_limiting(app: FastAPI) -> None:
23
+ """Register rate limiter and error handler on any FastAPI app."""
24
+ app.state.limiter = limiter
25
+ app.add_middleware(SlowAPIMiddleware)
26
+
27
+ @app.exception_handler(RateLimitExceeded)
28
+ async def _rate_limit_handler(request: Request, exc: RateLimitExceeded) -> JSONResponse:
29
+ return JSONResponse(
30
+ status_code=429,
31
+ content={
32
+ "success": False,
33
+ "error": {
34
+ "code": "RATE_LIMIT_EXCEEDED",
35
+ "message": f"Rate limit exceeded: {exc.detail}",
36
+ },
37
+ },
38
+ )
@@ -0,0 +1,66 @@
1
+ """
2
+ Redis client lifecycle and get_redis dependency.
3
+
4
+ Used by ALL microservices. No service-specific imports.
5
+ """
6
+
7
+ import logging
8
+ from collections.abc import AsyncGenerator
9
+
10
+ import redis.asyncio as aioredis
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ _redis_client: aioredis.Redis | None = None
15
+
16
+ MAX_CONNECT_RETRIES = 3
17
+
18
+
19
+ async def init_redis(
20
+ url: str,
21
+ socket_timeout: int = 10,
22
+ ) -> None:
23
+ """Create the async Redis client. Called during app startup."""
24
+ global _redis_client
25
+
26
+ logger.info("Connecting to Redis: %s", url.split("@")[-1] if "@" in url else url)
27
+
28
+ _redis_client = aioredis.from_url(
29
+ url,
30
+ socket_timeout=socket_timeout,
31
+ socket_connect_timeout=socket_timeout,
32
+ decode_responses=True,
33
+ )
34
+
35
+ for attempt in range(1, MAX_CONNECT_RETRIES + 1):
36
+ try:
37
+ await _redis_client.ping()
38
+ logger.info("Redis connection established.")
39
+ return
40
+ except (aioredis.ConnectionError, aioredis.TimeoutError) as exc:
41
+ logger.warning("Redis attempt %d/%d failed: %s", attempt, MAX_CONNECT_RETRIES, exc)
42
+ if attempt == MAX_CONNECT_RETRIES:
43
+ raise
44
+
45
+
46
+ async def close_redis() -> None:
47
+ """Close the Redis connection. Called during app shutdown."""
48
+ global _redis_client
49
+ if _redis_client:
50
+ await _redis_client.aclose()
51
+ logger.info("Redis connection closed.")
52
+ _redis_client = None
53
+
54
+
55
+ async def get_redis() -> AsyncGenerator[aioredis.Redis, None]:
56
+ """FastAPI dependency that yields the Redis client."""
57
+ if _redis_client is None:
58
+ raise RuntimeError("Redis not initialized. Call init_redis() first.")
59
+ yield _redis_client
60
+
61
+
62
+ def get_redis_client() -> aioredis.Redis:
63
+ """Return the raw Redis client (for non-DI contexts)."""
64
+ if _redis_client is None:
65
+ raise RuntimeError("Redis not initialized.")
66
+ return _redis_client
@@ -0,0 +1,15 @@
1
+ """
2
+ Base Pydantic schema for ALL microservices.
3
+ """
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+
8
+ class BaseSchema(BaseModel):
9
+ """Base schema for all request/response models across all services."""
10
+
11
+ model_config = ConfigDict(
12
+ from_attributes=True,
13
+ populate_by_name=True,
14
+ str_strip_whitespace=True,
15
+ )