ferrox-py 1.0.0__py3-none-any.whl

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 (90) hide show
  1. examples/data_platform/main.py +47 -0
  2. examples/data_platform/modules/analytics/quant_controller.py +75 -0
  3. examples/data_platform/modules/analytics/quant_service.py +86 -0
  4. examples/data_platform/modules/catalog/catalog_controller.py +40 -0
  5. examples/data_platform/modules/etl/etl_controller.py +51 -0
  6. examples/data_platform/modules/ingestion/ingestion_controller.py +30 -0
  7. examples/data_platform/modules/ingestion/ingestion_service.py +47 -0
  8. examples/data_platform/modules/live_data/live_controller.py +23 -0
  9. examples/data_platform/modules/live_data/live_service.py +70 -0
  10. examples/data_platform/modules/streaming/binance_ingestor.py +86 -0
  11. examples/data_platform/modules/streaming/data_contracts.py +42 -0
  12. examples/data_platform/static/index.html +394 -0
  13. ferrox_py/__init__.py +1 -0
  14. ferrox_py/abstractions/__init__.py +1 -0
  15. ferrox_py/abstractions/crud_generator.py +30 -0
  16. ferrox_py/abstractions/guards_advanced.py +9 -0
  17. ferrox_py/abstractions/interceptors.py +23 -0
  18. ferrox_py/abstractions/pipes.py +21 -0
  19. ferrox_py/architectures/__init__.py +1 -0
  20. ferrox_py/architectures/api_gateway.py +34 -0
  21. ferrox_py/architectures/events.py +16 -0
  22. ferrox_py/architectures/queues_jobs.py +45 -0
  23. ferrox_py/architectures/sagas.py +38 -0
  24. ferrox_py/architectures/scheduling.py +21 -0
  25. ferrox_py/cli/__init__.py +1 -0
  26. ferrox_py/cli/code_factory.py +2 -0
  27. ferrox_py/cli/commands.py +33 -0
  28. ferrox_py/core/__init__.py +1 -0
  29. ferrox_py/core/app.py +24 -0
  30. ferrox_py/core/config.py +19 -0
  31. ferrox_py/core/container.py +51 -0
  32. ferrox_py/core/controllers.py +16 -0
  33. ferrox_py/core/errors.py +16 -0
  34. ferrox_py/core/module.py +17 -0
  35. ferrox_py/core/provider.py +19 -0
  36. ferrox_py/core/testing.py +19 -0
  37. ferrox_py/cqrs/__init__.py +1 -0
  38. ferrox_py/cqrs/bus.py +29 -0
  39. ferrox_py/databases/__init__.py +1 -0
  40. ferrox_py/databases/migrations.py +7 -0
  41. ferrox_py/databases/mongodb.py +14 -0
  42. ferrox_py/databases/redis.py +24 -0
  43. ferrox_py/databases/sqlalchemy.py +17 -0
  44. ferrox_py/integrations/__init__.py +1 -0
  45. ferrox_py/integrations/cloud.py +10 -0
  46. ferrox_py/integrations/feature_flags.py +9 -0
  47. ferrox_py/integrations/i18n.py +11 -0
  48. ferrox_py/integrations/mailer.py +33 -0
  49. ferrox_py/integrations/notifications.py +9 -0
  50. ferrox_py/integrations/payments.py +31 -0
  51. ferrox_py/integrations/search.py +9 -0
  52. ferrox_py/integrations/webhooks.py +12 -0
  53. ferrox_py/observability/__init__.py +1 -0
  54. ferrox_py/observability/health.py +14 -0
  55. ferrox_py/observability/logging.py +20 -0
  56. ferrox_py/observability/metrics.py +20 -0
  57. ferrox_py/observability/tracing.py +22 -0
  58. ferrox_py/pipelines/__init__.py +4 -0
  59. ferrox_py/pipelines/dag.py +72 -0
  60. ferrox_py/pipelines/node.py +31 -0
  61. ferrox_py/resilience/__init__.py +1 -0
  62. ferrox_py/resilience/circuit_breaker.py +37 -0
  63. ferrox_py/security/__init__.py +1 -0
  64. ferrox_py/security/advanced_auth.py +25 -0
  65. ferrox_py/security/distributed_locks.py +40 -0
  66. ferrox_py/security/headers.py +20 -0
  67. ferrox_py/security/jwt.py +51 -0
  68. ferrox_py/security/mtd.py +18 -0
  69. ferrox_py/security/paseto.py +17 -0
  70. ferrox_py/security/rate_limiting.py +38 -0
  71. ferrox_py/security/selftest.py +6 -0
  72. ferrox_py/security/sentinel.py +21 -0
  73. ferrox_py/transports/__init__.py +1 -0
  74. ferrox_py/transports/datagrid.py +33 -0
  75. ferrox_py/transports/file_storage.py +24 -0
  76. ferrox_py/transports/graphql.py +11 -0
  77. ferrox_py/transports/sse.py +17 -0
  78. ferrox_py/web/__init__.py +1 -0
  79. ferrox_py/web/decorators.py +29 -0
  80. ferrox_py/web/guards.py +14 -0
  81. ferrox_py-1.0.0.dist-info/METADATA +95 -0
  82. ferrox_py-1.0.0.dist-info/RECORD +90 -0
  83. ferrox_py-1.0.0.dist-info/WHEEL +4 -0
  84. ferrox_py-1.0.0.dist-info/entry_points.txt +2 -0
  85. infrastructure/README.md +69 -0
  86. infrastructure/terraform/aws/main.tf +89 -0
  87. infrastructure/terraform/aws/variables.tf +14 -0
  88. infrastructure/terraform/gcp/main.tf +64 -0
  89. infrastructure/terraform/gcp/variables.tf +8 -0
  90. infrastructure/terraform/vps/main.tf +117 -0
@@ -0,0 +1,47 @@
1
+ import uvicorn
2
+ from fastapi import FastAPI
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.responses import FileResponse
5
+ from ferrox_py.core.container import Container
6
+ from modules.ingestion.ingestion_controller import IngestionController
7
+ from modules.catalog.catalog_controller import CatalogController
8
+ from modules.live_data.live_controller import LiveController
9
+ from modules.etl.etl_controller import EtlController
10
+ from modules.analytics.quant_controller import QuantController
11
+ import os
12
+
13
+ def create_app() -> FastAPI:
14
+ app = FastAPI(title="Ferrox Data Platform")
15
+
16
+ # Mount Static Files for the Dashboard
17
+ static_dir = os.path.join(os.path.dirname(__file__), "static")
18
+ if os.path.exists(static_dir):
19
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
20
+
21
+ @app.get("/")
22
+ async def serve_dashboard():
23
+ return FileResponse(os.path.join(static_dir, "index.html"))
24
+
25
+ # 1. Boot IoC Container
26
+ container = Container()
27
+
28
+ # 2. Register Controllers
29
+ ingest = IngestionController(container)
30
+ catalog = CatalogController(container)
31
+ live = LiveController(container)
32
+ etl = EtlController(container)
33
+ quant = QuantController(container)
34
+
35
+ # 3. Mount Routes
36
+ app.include_router(ingest.router)
37
+ app.include_router(catalog.router)
38
+ app.include_router(live.router)
39
+ app.include_router(etl.router)
40
+ app.include_router(quant.router)
41
+
42
+ return app
43
+
44
+ app = create_app()
45
+
46
+ if __name__ == "__main__":
47
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
@@ -0,0 +1,75 @@
1
+ from fastapi import APIRouter
2
+ from ferrox_py.pipelines.node import PipelineNode
3
+ from ferrox_py.pipelines.dag import DAG
4
+ from .quant_service import QuantitativeAnalyticsService
5
+ import httpx
6
+ import random
7
+
8
+ class QuantController:
9
+ def __init__(self, container):
10
+ self.router = APIRouter(prefix="/analytics", tags=["Quantitative Analysis"])
11
+ self.container = container
12
+
13
+ if QuantitativeAnalyticsService not in self.container._providers:
14
+ self.container._providers[QuantitativeAnalyticsService] = QuantitativeAnalyticsService
15
+
16
+ self.setup_routes()
17
+
18
+ def setup_routes(self):
19
+ @self.router.get("/signals")
20
+ async def get_market_signals():
21
+ """
22
+ Uses the DAG Orchestrator to build a real-time signal report.
23
+ 1. Extract BTC/ETH data
24
+ 2. Compute Z-Scores and Correlats
25
+ 3. Compute Order Book Imbalance
26
+ """
27
+ service: QuantitativeAnalyticsService = self.container.resolve(QuantitativeAnalyticsService)
28
+
29
+ dag = DAG(name="Quant-Signals-Pipeline")
30
+
31
+ # --- NODE 1: Fetch Live Market Context ---
32
+ async def fetch_context():
33
+ async with httpx.AsyncClient() as client:
34
+ res = await client.get("https://api.binance.com/api/v3/ticker/bookTicker?symbol=BTCUSDT")
35
+ data = res.json()
36
+ # Convert to depth format: [[price, qty]]
37
+ bids = [[float(data["bidPrice"]), float(data["bidQty"])]]
38
+ asks = [[float(data["askPrice"]), float(data["askQty"])]]
39
+
40
+ # Randomize a massive trade volume for demo purposes sometimes
41
+ sim_trade_vol = random.choice([0.1, 0.5, 1.2, 5.0, 50.0, 150.0])
42
+ return {"bids": bids, "asks": asks, "last_trade_vol": sim_trade_vol, "price": float(data["bidPrice"])}
43
+
44
+ extract_node = PipelineNode("extract_market", fetch_context)
45
+
46
+ # --- NODE 2: OBI Analysis ---
47
+ def compute_obi(extract_res):
48
+ data = extract_res["extract_market"]
49
+ return service.compute_order_book_imbalance(data["bids"], data["asks"])
50
+
51
+ obi_node = PipelineNode("obi_analysis", lambda: None) # We will inject logic manually or just use closure
52
+
53
+ # Since our simple DAG engine doesn't inject results into kwargs yet, we use a shared state or closure
54
+ # For this demo, we can just run it inline
55
+
56
+ async def run_analysis():
57
+ context = await fetch_context()
58
+
59
+ obi = service.compute_order_book_imbalance(context["bids"], context["asks"])
60
+ whale = service.detect_whale_anomalies(context["price"], context["last_trade_vol"])
61
+
62
+ # Simulate BTC/ETH prices over last 10 ticks for correlation
63
+ btc_prices = [context["price"] * (1 + random.uniform(-0.01, 0.01)) for _ in range(10)]
64
+ eth_prices = [3000 * (1 + random.uniform(-0.01, 0.01)) for _ in range(10)]
65
+ corr = service.compute_cross_asset_correlation(btc_prices, eth_prices)
66
+
67
+ return {
68
+ "asset": "BTC",
69
+ "order_book_imbalance": obi,
70
+ "whale_detection": whale,
71
+ "btc_eth_correlation": corr
72
+ }
73
+
74
+ # Execute directly for the API response
75
+ return await run_analysis()
@@ -0,0 +1,86 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import random
4
+ from typing import Dict, Any, List
5
+ from structlog import get_logger
6
+
7
+ logger = get_logger()
8
+
9
+ class QuantitativeAnalyticsService:
10
+ """
11
+ Computes statistical anomalies and order book imbalances.
12
+ In a real scenario, this extracts the last N minutes of data from Redis.
13
+ For this demo, we simulate the historical distribution based on current data.
14
+ """
15
+
16
+ def compute_order_book_imbalance(self, bids: List[List[float]], asks: List[List[float]]) -> Dict[str, Any]:
17
+ """
18
+ OBI = (BidVolume - AskVolume) / (BidVolume + AskVolume)
19
+ Values near +1 indicate heavy buy-side spoofing/pressure.
20
+ Values near -1 indicate heavy sell-side spoofing/pressure.
21
+ """
22
+ if not bids or not asks:
23
+ return {"obi": 0.0, "status": "Neutral", "spoofing_detected": False}
24
+
25
+ bid_vol = sum(price * qty for price, qty in bids)
26
+ ask_vol = sum(price * qty for price, qty in asks)
27
+
28
+ total = bid_vol + ask_vol
29
+ if total == 0:
30
+ return {"obi": 0.0, "status": "Neutral", "spoofing_detected": False}
31
+
32
+ obi = (bid_vol - ask_vol) / total
33
+
34
+ status = "Neutral"
35
+ spoofing = False
36
+ if obi > 0.7:
37
+ status = "Extreme Buy Pressure"
38
+ spoofing = obi > 0.9
39
+ elif obi < -0.7:
40
+ status = "Extreme Sell Pressure"
41
+ spoofing = obi < -0.9
42
+
43
+ return {
44
+ "obi": round(obi, 4),
45
+ "status": status,
46
+ "spoofing_detected": spoofing,
47
+ "bid_volume": round(bid_vol, 2),
48
+ "ask_volume": round(ask_vol, 2)
49
+ }
50
+
51
+ def detect_whale_anomalies(self, current_price: float, current_volume: float) -> Dict[str, Any]:
52
+ """
53
+ Computes Z-Score against a simulated historical rolling window.
54
+ Z = (x - mean) / std_dev
55
+ If Z > 3, it's a massive anomaly (Whale).
56
+ """
57
+ # Simulate last 100 trades volume distribution (Log-normal distribution typical for crypto volumes)
58
+ simulated_history = np.random.lognormal(mean=np.log(current_volume*0.2), sigma=0.8, size=100)
59
+ df = pd.DataFrame(simulated_history, columns=['volume'])
60
+
61
+ mean_vol = df['volume'].mean()
62
+ std_vol = df['volume'].std()
63
+
64
+ # Inject the current volume to see how it compares
65
+ z_score = (current_volume - mean_vol) / (std_vol if std_vol > 0 else 1)
66
+
67
+ is_whale = z_score > 3.0
68
+
69
+ return {
70
+ "z_score": round(z_score, 2),
71
+ "mean_volume": round(mean_vol, 2),
72
+ "is_whale_anomaly": bool(is_whale),
73
+ "severity": "CRITICAL" if z_score > 5 else ("HIGH" if is_whale else "NORMAL")
74
+ }
75
+
76
+ def compute_cross_asset_correlation(self, asset1_prices: List[float], asset2_prices: List[float]) -> float:
77
+ """
78
+ Pearson correlation coefficient between two assets.
79
+ """
80
+ if len(asset1_prices) != len(asset2_prices) or len(asset1_prices) < 2:
81
+ return 0.0
82
+
83
+ df = pd.DataFrame({'a1': asset1_prices, 'a2': asset2_prices})
84
+ corr = df['a1'].corr(df['a2'])
85
+
86
+ return round(corr, 4) if not pd.isna(corr) else 0.0
@@ -0,0 +1,40 @@
1
+ from fastapi import APIRouter
2
+ from ferrox_py.core.controllers import BaseController
3
+ from ferrox_py.core.container import Container
4
+ from ferrox_py.transports.datagrid import DataGridHelper
5
+ from ferrox_py.databases.mongodb import MongoService
6
+
7
+ class CatalogController(BaseController):
8
+ def __init__(self, container: Container):
9
+ super().__init__(prefix="/catalog", tags=["Catalog"])
10
+ self.container = container
11
+
12
+ # Lazy dependency injection
13
+ # (In a real app, container resolves these automatically via @module)
14
+ self.datagrid = DataGridHelper()
15
+ self.mongo = MongoService()
16
+
17
+ @self.router.get("/datasets")
18
+ async def list_datasets(skip: int = 0, take: int = 10, sort: str = None, filter: str = None):
19
+ """
20
+ Queries the Data Catalog metadata using DataGrid abstractions.
21
+ """
22
+ query_params = {"skip": skip, "take": take}
23
+ if sort: query_params["sort"] = sort
24
+ if filter: query_params["filter"] = filter
25
+
26
+ ast = self.datagrid.parse_query(query_params)
27
+
28
+ # Simulated Mongo cursor execution
29
+ # db = self.mongo.get_database("data_platform")
30
+ # cursor = db.catalog.find(ast.filters).skip(ast.skip).limit(ast.take)
31
+
32
+ return self.ok({
33
+ "query_ast": {
34
+ "skip": ast.skip,
35
+ "take": ast.take,
36
+ "filters": ast.filters,
37
+ "sort": ast.sort
38
+ },
39
+ "results": ["dataset_weather", "dataset_finance"]
40
+ }, "Catalog queried successfully")
@@ -0,0 +1,51 @@
1
+ import asyncio
2
+ from fastapi import APIRouter
3
+ from ferrox_py.pipelines.node import PipelineNode
4
+ from ferrox_py.pipelines.dag import DAG
5
+
6
+ class EtlController:
7
+ def __init__(self, container):
8
+ self.router = APIRouter(prefix="/etl", tags=["Data Pipelines (DAGs)"])
9
+ self.container = container
10
+ self.setup_routes()
11
+
12
+ def setup_routes(self):
13
+ @self.router.get("/trigger")
14
+ async def trigger_pipeline():
15
+ """
16
+ Demonstrates Ferrox-Py's internal DAG engine for ETL workloads.
17
+ """
18
+ dag = DAG(name="Daily-Aggregations")
19
+
20
+ # Create nodes (Steps in ETL)
21
+ extract_users = PipelineNode("extract_users", lambda: {"users": 5000})
22
+ extract_sales = PipelineNode("extract_sales", lambda: {"sales": 120000})
23
+
24
+ async def transform_data():
25
+ await asyncio.sleep(0.1) # simulate heavy transformation
26
+ return {"transformed": True}
27
+
28
+ transform = PipelineNode("transform", transform_data)
29
+ transform.depends_on("extract_users", "extract_sales")
30
+
31
+ async def load_to_warehouse():
32
+ await asyncio.sleep(0.1) # simulate db insert
33
+ return {"loaded": True}
34
+
35
+ load = PipelineNode("load", load_to_warehouse)
36
+ load.depends_on("transform")
37
+
38
+ # Register in DAG
39
+ dag.add_node(extract_users)
40
+ dag.add_node(extract_sales)
41
+ dag.add_node(transform)
42
+ dag.add_node(load)
43
+
44
+ # Execute the graph
45
+ results = await dag.execute()
46
+
47
+ return {
48
+ "message": "ETL Pipeline Executed Successfully",
49
+ "dag_name": dag.name,
50
+ "execution_graph": results
51
+ }
@@ -0,0 +1,30 @@
1
+ from fastapi import Request
2
+ from ferrox_py.core.controllers import BaseController
3
+ from ferrox_py.core.container import Container
4
+ from ferrox_py.security.rate_limiting import RateLimiter
5
+ from .ingestion_service import IngestionService
6
+
7
+ class IngestionController(BaseController):
8
+ def __init__(self, container: Container):
9
+ super().__init__(prefix="/ingest", tags=["Ingestion"])
10
+
11
+ self.service = IngestionService()
12
+ self.rate_limiter = RateLimiter()
13
+
14
+ @self.router.post("/trigger/weather")
15
+ async def trigger_weather_etl(request: Request):
16
+ """
17
+ Triggers a data pipeline to fetch weather data.
18
+ Protected by Token Bucket Rate Limiting.
19
+ """
20
+ client_ip = request.client.host if request.client else "unknown"
21
+
22
+ # Rate limit: 2 requests per second
23
+ allowed = await self.rate_limiter.is_allowed(f"etl_{client_ip}", capacity=2, refill_rate=1.0)
24
+ if not allowed:
25
+ return {"status": "error", "message": "Too Many Requests", "code": 429}
26
+
27
+ # Execute Pipeline
28
+ result = await self.service.run_weather_pipeline()
29
+
30
+ return self.ok(result, "Pipeline executed successfully")
@@ -0,0 +1,47 @@
1
+ from ferrox_py_utils.pipelines.orchestrator import PipelineOrchestrator, PipelineStep
2
+ from ferrox_py_utils.schemas.registry import SchemaRegistry
3
+ from ferrox_py.core.provider import injectable
4
+ from typing import Any
5
+
6
+ @injectable()
7
+ class IngestionService:
8
+ def __init__(self):
9
+ self.orchestrator = PipelineOrchestrator()
10
+ self.registry = SchemaRegistry()
11
+
12
+ # Register dynamic schema for weather metrics
13
+ self.registry.register_schema("WeatherMetric", {
14
+ "city": (str, ...),
15
+ "temperature": (float, ...),
16
+ "humidity": (int, ...)
17
+ })
18
+
19
+ async def _extract(self, payload: Any) -> Any:
20
+ # Simulate downloading data from HTTP Connector
21
+ print("Extracting data...")
22
+ return {"city": "Milan", "temperature": 22.5, "humidity": "60"} # humidity is string, should be casted to int
23
+
24
+ async def _transform(self, data: Any) -> Any:
25
+ print("Transforming and Validating data...")
26
+ # Validate against dynamic schema
27
+ clean_data = self.registry.validate("WeatherMetric", data)
28
+ return clean_data
29
+
30
+ async def _load(self, data: Any) -> Any:
31
+ print("Loading data into MongoDB...")
32
+ # Simulate Mongo load
33
+ return {"inserted_id": "5f1b2c3d", "data": data}
34
+
35
+ async def run_weather_pipeline(self) -> dict:
36
+ steps = [
37
+ PipelineStep("Extract", self._extract),
38
+ PipelineStep("Transform", self._transform),
39
+ PipelineStep("Load", self._load)
40
+ ]
41
+
42
+ result = await self.orchestrator.execute_pipeline(
43
+ name="Weather_ETL",
44
+ initial_data=None,
45
+ steps=steps
46
+ )
47
+ return result
@@ -0,0 +1,23 @@
1
+ from fastapi import APIRouter, Depends
2
+ from .live_service import LiveDataAggregatorService
3
+
4
+ class LiveController:
5
+ def __init__(self, container):
6
+ self.router = APIRouter(prefix="/live", tags=["Live Data Platform Demo"])
7
+ self.container = container
8
+
9
+ # Register the service in the IoC Container dynamically for the demo
10
+ if LiveDataAggregatorService not in self.container._providers:
11
+ self.container._providers[LiveDataAggregatorService] = LiveDataAggregatorService
12
+
13
+ self.setup_routes()
14
+
15
+ def setup_routes(self):
16
+ @self.router.get("/dashboard")
17
+ async def get_live_dashboard():
18
+ """
19
+ Fetches real-time aggregated data from Binance, GitHub, and Open-Meteo.
20
+ Demonstrates Ferrox-Py's async I/O capabilities.
21
+ """
22
+ service: LiveDataAggregatorService = self.container.resolve(LiveDataAggregatorService)
23
+ return await service.get_aggregated_dashboard()
@@ -0,0 +1,70 @@
1
+ import httpx
2
+ import asyncio
3
+ from structlog import get_logger
4
+
5
+ logger = get_logger()
6
+
7
+ class LiveDataAggregatorService:
8
+ """
9
+ Demonstrates Ferrox-Py's capability to act as a high-performance Data Platform Edge.
10
+ Fetches data concurrently from multiple public, free APIs.
11
+ """
12
+
13
+ def __init__(self):
14
+ pass
15
+
16
+ async def fetch_crypto_price(self, client: httpx.AsyncClient) -> dict:
17
+ try:
18
+ # Binance public API (no key required for ticker)
19
+ res = await client.get("https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT")
20
+ res.raise_for_status()
21
+ data = res.json()
22
+ return {"asset": "BTC", "price_usd": float(data["price"])}
23
+ except Exception as e:
24
+ logger.error("failed_fetch_crypto", error=str(e))
25
+ return {"asset": "BTC", "price_usd": None, "error": "API Unavailable"}
26
+
27
+ async def fetch_github_status(self, client: httpx.AsyncClient) -> dict:
28
+ try:
29
+ # GitHub public API
30
+ res = await client.get("https://www.githubstatus.com/api/v2/status.json")
31
+ res.raise_for_status()
32
+ data = res.json()
33
+ return {"system": "GitHub", "status": data.get("status", {}).get("description")}
34
+ except Exception as e:
35
+ logger.error("failed_fetch_github", error=str(e))
36
+ return {"system": "GitHub", "status": "Unknown"}
37
+
38
+ async def fetch_weather_ny(self, client: httpx.AsyncClient) -> dict:
39
+ try:
40
+ # Open-Meteo (Free, no key)
41
+ res = await client.get("https://api.open-meteo.com/v1/forecast?latitude=40.7143&longitude=-74.006&current_weather=true")
42
+ res.raise_for_status()
43
+ data = res.json()
44
+ current = data.get("current_weather", {})
45
+ return {"city": "New York", "temperature_celsius": current.get("temperature"), "windspeed": current.get("windspeed")}
46
+ except Exception as e:
47
+ logger.error("failed_fetch_weather", error=str(e))
48
+ return {"city": "New York", "error": "Weather API Unavailable"}
49
+
50
+ async def get_aggregated_dashboard(self) -> dict:
51
+ """
52
+ Aggregates data using asyncio.gather for lightning-fast concurrent I/O.
53
+ """
54
+ async with httpx.AsyncClient(timeout=5.0) as client:
55
+ crypto_task = self.fetch_crypto_price(client)
56
+ github_task = self.fetch_github_status(client)
57
+ weather_task = self.fetch_weather_ny(client)
58
+
59
+ # Execute all external calls concurrently (Scatter-Gather pattern)
60
+ results = await asyncio.gather(crypto_task, github_task, weather_task)
61
+
62
+ return {
63
+ "platform": "Ferrox-Py Data Aggregator",
64
+ "status": "Operational",
65
+ "metrics": {
66
+ "markets": results[0],
67
+ "dev_ops": results[1],
68
+ "environment": results[2]
69
+ }
70
+ }
@@ -0,0 +1,86 @@
1
+ import asyncio
2
+ import json
3
+ import websockets
4
+ import redis.asyncio as redis
5
+ from pydantic import ValidationError
6
+ from structlog import get_logger
7
+ from .data_contracts import BinanceTradeContract, BinanceDepthContract, DataLineage
8
+
9
+ logger = get_logger()
10
+
11
+ # Multiplexed Binance WebSocket for multi-asset Trades & OrderBooks
12
+ # We ingest Trades for BTC, ETH, SOL (for Correlation & Whales)
13
+ # We ingest Depth5 for BTC, ETH, SOL (for OrderBook Imbalance/Spoofing)
14
+ STREAMS = [
15
+ "btcusdt@trade", "ethusdt@trade", "solusdt@trade",
16
+ "btcusdt@depth5@100ms", "ethusdt@depth5@100ms", "solusdt@depth5@100ms"
17
+ ]
18
+ BINANCE_WS_URL = f"wss://stream.binance.com:9443/stream?streams={'/'.join(STREAMS)}"
19
+
20
+ REDIS_STREAM_TRADES = "ferrox:ingestion:crypto_trades"
21
+ REDIS_STREAM_DEPTH = "ferrox:ingestion:crypto_depth"
22
+
23
+ async def run_ingestion_engine():
24
+ logger.info("Starting Multi-Asset High-Throughput Ingestion Engine...")
25
+
26
+ r = redis.Redis(host='localhost', port=6379, decode_responses=True, socket_connect_timeout=1.0)
27
+ use_redis = True
28
+ try:
29
+ await asyncio.wait_for(r.ping(), timeout=2.0)
30
+ logger.info("Connected to Redis for Stream Backpressure.")
31
+ except Exception:
32
+ logger.warning("Redis not found or timed out. Running in Simulation/Print mode.")
33
+ use_redis = False
34
+
35
+ async for websocket in websockets.connect(BINANCE_WS_URL):
36
+ logger.info("Connected to Binance Multiplexed WebSocket", streams=len(STREAMS))
37
+ try:
38
+ async for message in websocket:
39
+ payload = json.loads(message)
40
+ stream_name = payload.get("stream")
41
+ raw_data = payload.get("data")
42
+
43
+ if not stream_name or not raw_data:
44
+ continue
45
+
46
+ lineage = DataLineage(source_system="Binance_WS_API")
47
+
48
+ # Route based on stream type
49
+ try:
50
+ if "@trade" in stream_name:
51
+ contract = BinanceTradeContract(**raw_data)
52
+ contract.lineage = lineage
53
+
54
+ if use_redis:
55
+ await r.xadd(REDIS_STREAM_TRADES, {"payload": contract.model_dump_json()}, maxlen=100000)
56
+ else:
57
+ logger.info("trade_ingested", symbol=contract.symbol, price=contract.price)
58
+
59
+ elif "@depth5" in stream_name:
60
+ # Depth payload doesn't contain symbol, we extract it from stream name (e.g. btcusdt@depth5)
61
+ symbol = stream_name.split('@')[0].upper()
62
+ raw_data['symbol'] = symbol
63
+ contract = BinanceDepthContract(**raw_data)
64
+ contract.lineage = lineage
65
+
66
+ if use_redis:
67
+ await r.xadd(REDIS_STREAM_DEPTH, {"payload": contract.model_dump_json()}, maxlen=50000)
68
+ else:
69
+ # Log just top bid/ask to avoid console spam
70
+ top_bid = contract.bids[0][0] if contract.bids else 0
71
+ top_ask = contract.asks[0][0] if contract.asks else 0
72
+ logger.info("depth_ingested", symbol=contract.symbol, top_bid=top_bid, top_ask=top_ask)
73
+
74
+ except ValidationError as e:
75
+ logger.error("schema_drift_detected", stream=stream_name, error=str(e))
76
+ continue
77
+
78
+ except websockets.ConnectionClosed:
79
+ logger.warning("WebSocket Connection Closed. Reconnecting...")
80
+ continue
81
+ except Exception as e:
82
+ logger.error("fatal_ingestion_error", error=str(e))
83
+ break
84
+
85
+ if __name__ == "__main__":
86
+ asyncio.run(run_ingestion_engine())
@@ -0,0 +1,42 @@
1
+ from pydantic import BaseModel, Field, field_validator
2
+ from datetime import datetime
3
+ from typing import Optional, List
4
+
5
+ class DataLineage(BaseModel):
6
+ """
7
+ Solves Point 3 (Lineage & Governance):
8
+ Every event MUST carry its origin, ingestion timestamp, and processor version.
9
+ """
10
+ source_system: str
11
+ ingested_at: datetime = Field(default_factory=datetime.utcnow)
12
+ processor_version: str = "v1.1.0"
13
+
14
+
15
+ class BinanceTradeContract(BaseModel):
16
+ """
17
+ Validates real-time Trade executions.
18
+ """
19
+ event_type: str = Field(alias="e")
20
+ event_time: int = Field(alias="E")
21
+ symbol: str = Field(alias="s")
22
+ trade_id: int = Field(alias="t")
23
+ price: float = Field(alias="p")
24
+ quantity: float = Field(alias="q")
25
+ is_buyer_maker: bool = Field(alias="m")
26
+
27
+ lineage: Optional[DataLineage] = None
28
+
29
+ @field_validator("price", "quantity", mode="before")
30
+ def parse_floats(cls, v):
31
+ return float(v)
32
+
33
+ class BinanceDepthContract(BaseModel):
34
+ """
35
+ Validates Order Book Depth (Top 5 Bids/Asks) for detecting Spoofing and Imbalances.
36
+ """
37
+ last_update_id: int = Field(alias="lastUpdateId")
38
+ bids: List[List[float]]
39
+ asks: List[List[float]]
40
+ symbol: str = "UNKNOWN" # Injected by the multiplexer
41
+
42
+ lineage: Optional[DataLineage] = None