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.
- ai4i_core-1.0.0/LICENSE +21 -0
- ai4i_core-1.0.0/PKG-INFO +109 -0
- ai4i_core-1.0.0/README.md +50 -0
- ai4i_core-1.0.0/ai4i_core/__init__.py +13 -0
- ai4i_core-1.0.0/ai4i_core/bootstrap/__init__.py +32 -0
- ai4i_core-1.0.0/ai4i_core/bootstrap/cache.py +31 -0
- ai4i_core-1.0.0/ai4i_core/bootstrap/database.py +75 -0
- ai4i_core-1.0.0/ai4i_core/bootstrap/factory.py +114 -0
- ai4i_core-1.0.0/ai4i_core/bootstrap/health.py +64 -0
- ai4i_core-1.0.0/ai4i_core/bootstrap/rate_limit.py +38 -0
- ai4i_core-1.0.0/ai4i_core/bootstrap/redis.py +66 -0
- ai4i_core-1.0.0/ai4i_core/bootstrap/schemas.py +15 -0
- ai4i_core-1.0.0/ai4i_core/bootstrap/versioning.py +119 -0
- ai4i_core-1.0.0/ai4i_core/context.py +67 -0
- ai4i_core-1.0.0/ai4i_core/email/__init__.py +18 -0
- ai4i_core-1.0.0/ai4i_core/email/client.py +52 -0
- ai4i_core-1.0.0/ai4i_core/email/exceptions.py +10 -0
- ai4i_core-1.0.0/ai4i_core/email/fastapi.py +34 -0
- ai4i_core-1.0.0/ai4i_core/email/message.py +87 -0
- ai4i_core-1.0.0/ai4i_core/email/providers/__init__.py +11 -0
- ai4i_core-1.0.0/ai4i_core/email/providers/base.py +21 -0
- ai4i_core-1.0.0/ai4i_core/email/providers/console.py +50 -0
- ai4i_core-1.0.0/ai4i_core/email/providers/factory.py +65 -0
- ai4i_core-1.0.0/ai4i_core/email/providers/smtp.py +72 -0
- ai4i_core-1.0.0/ai4i_core/email/settings.py +77 -0
- ai4i_core-1.0.0/ai4i_core/email/templates.py +37 -0
- ai4i_core-1.0.0/ai4i_core/exceptions/__init__.py +114 -0
- ai4i_core-1.0.0/ai4i_core/exceptions/exceptions.py +329 -0
- ai4i_core-1.0.0/ai4i_core/exceptions/handlers.py +401 -0
- ai4i_core-1.0.0/ai4i_core/exceptions/responses.py +26 -0
- ai4i_core-1.0.0/ai4i_core/logging/__init__.py +46 -0
- ai4i_core-1.0.0/ai4i_core/logging/config.py +56 -0
- ai4i_core-1.0.0/ai4i_core/logging/context.py +27 -0
- ai4i_core-1.0.0/ai4i_core/logging/formatters.py +101 -0
- ai4i_core-1.0.0/ai4i_core/logging/logger.py +119 -0
- ai4i_core-1.0.0/ai4i_core/logging/middleware.py +117 -0
- ai4i_core-1.0.0/ai4i_core/observability/__init__.py +22 -0
- ai4i_core-1.0.0/ai4i_core/observability/config.py +22 -0
- ai4i_core-1.0.0/ai4i_core/observability/metrics.py +312 -0
- ai4i_core-1.0.0/ai4i_core/observability/middleware.py +543 -0
- ai4i_core-1.0.0/ai4i_core/observability/plugin.py +52 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/__init__.py +63 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/propagator.py +95 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/registry.py +411 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/trace_middleware.py +22 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/trace_wrapper.py +114 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/traceability.py +277 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/util/asr/stages.json +43 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/util/audio_language_detection/stages.json +43 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/util/language_detection/stages.json +43 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/util/language_diarization/stages.json +43 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/util/ner/stages.json +43 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/util/nmt/stages.json +43 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/util/ocr/stages.json +43 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/util/speakerdiarization/stages.json +43 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/util/transliteration/stages.json +43 -0
- ai4i_core-1.0.0/ai4i_core/telemetry/util/tts/stages.json +43 -0
- ai4i_core-1.0.0/ai4i_core.egg-info/PKG-INFO +109 -0
- ai4i_core-1.0.0/ai4i_core.egg-info/SOURCES.txt +62 -0
- ai4i_core-1.0.0/ai4i_core.egg-info/dependency_links.txt +1 -0
- ai4i_core-1.0.0/ai4i_core.egg-info/requires.txt +32 -0
- ai4i_core-1.0.0/ai4i_core.egg-info/top_level.txt +1 -0
- ai4i_core-1.0.0/pyproject.toml +100 -0
- ai4i_core-1.0.0/setup.cfg +4 -0
ai4i_core-1.0.0/LICENSE
ADDED
|
@@ -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.
|
ai4i_core-1.0.0/PKG-INFO
ADDED
|
@@ -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
|
+
)
|