logic-fingerprint 0.1.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.
- logic_fingerprint-0.1.0/PKG-INFO +178 -0
- logic_fingerprint-0.1.0/README.md +164 -0
- logic_fingerprint-0.1.0/pyproject.toml +38 -0
- logic_fingerprint-0.1.0/setup.cfg +4 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/__init__.py +2 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/api.py +86 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/app_factory.py +13 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/config.py +9 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/consensus.py +11 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/context_builder.py +21 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/errors.py +49 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/executor.py +52 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/fsm.py +77 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/handlers.py +26 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/heartbeat.py +10 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/input_models.py +4 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/lifespan.py +21 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/metrics.py +15 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/middleware.py +65 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/models.py +57 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/output_models.py +4 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/prometheus_metrics.py +26 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/protect.py +269 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/redis_consensus.py +48 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/runtime.py +49 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/service.py +12 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint/validator.py +20 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint.egg-info/PKG-INFO +178 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint.egg-info/SOURCES.txt +38 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint.egg-info/dependency_links.txt +1 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint.egg-info/requires.txt +1 -0
- logic_fingerprint-0.1.0/src/logic_fingerprint.egg-info/top_level.txt +1 -0
- logic_fingerprint-0.1.0/tests/test_api.py +29 -0
- logic_fingerprint-0.1.0/tests/test_context_builder.py +17 -0
- logic_fingerprint-0.1.0/tests/test_executor.py +38 -0
- logic_fingerprint-0.1.0/tests/test_fsm.py +28 -0
- logic_fingerprint-0.1.0/tests/test_middleware.py +35 -0
- logic_fingerprint-0.1.0/tests/test_prometheus_metrics.py +20 -0
- logic_fingerprint-0.1.0/tests/test_redis_ttl_backend.py +36 -0
- logic_fingerprint-0.1.0/tests/test_runtime.py +8 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logic-fingerprint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Execution safety layer for functions, APIs, and LLMs.
|
|
5
|
+
Author: Your Name
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: llm,circuit-breaker,middleware,reliability,api,decorator
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: pydantic>=1.10
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Add a safety layer to any function.
|
|
17
|
+
|
|
18
|
+
Logic Fingerprint protects your function/LLM/API calls with:
|
|
19
|
+
|
|
20
|
+
circuit breaker (no retry storms)
|
|
21
|
+
safe recovery (HALF_OPEN probing)
|
|
22
|
+
schema validation (stable outputs)
|
|
23
|
+
unified error handling
|
|
24
|
+
|
|
25
|
+
Works with a single decorator: @protect()
|
|
26
|
+
|
|
27
|
+
## 🧭 How it works
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
Your Function
|
|
31
|
+
↓
|
|
32
|
+
@protect
|
|
33
|
+
↓
|
|
34
|
+
Logic Fingerprint
|
|
35
|
+
├─ Circuit Breaker
|
|
36
|
+
├─ Probe Recovery
|
|
37
|
+
├─ Validation Layer
|
|
38
|
+
├─ Error Control
|
|
39
|
+
↓
|
|
40
|
+
Safe Execution
|
|
41
|
+
↓
|
|
42
|
+
Result / Error
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## ⚡ Quick Start
|
|
46
|
+
|
|
47
|
+
### Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install -r requirements.txt
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Protect a real local LLM call
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from logic_fingerprint import protect
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@protect()
|
|
61
|
+
def ask_local_llm(request):
|
|
62
|
+
import json
|
|
63
|
+
import urllib.request
|
|
64
|
+
|
|
65
|
+
payload = {
|
|
66
|
+
"model": "llama3.2",
|
|
67
|
+
"prompt": request.payload["prompt"],
|
|
68
|
+
"stream": False,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
req = urllib.request.Request(
|
|
72
|
+
"http://127.0.0.1:11434/api/generate",
|
|
73
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
74
|
+
headers={"Content-Type": "application/json"},
|
|
75
|
+
method="POST",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
79
|
+
body = json.loads(resp.read().decode("utf-8"))
|
|
80
|
+
|
|
81
|
+
return {"answer": body["response"].strip()}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Call it like a normal function
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
result = ask_local_llm({
|
|
88
|
+
"prompt": "Explain circuit breaker in one sentence."
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
print(result)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Output:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
{'answer': 'A circuit breaker is a safety device that automatically stops an overcurrent flow in an electrical circuit to prevent damage to equipment and potential hazards.'}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### What you get automatically
|
|
101
|
+
|
|
102
|
+
* circuit breaker protection
|
|
103
|
+
* safe recovery probing
|
|
104
|
+
* controlled failure handling
|
|
105
|
+
* optional schema validation
|
|
106
|
+
* auto request context
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
```md
|
|
111
|
+
## 🎬 Demos
|
|
112
|
+
|
|
113
|
+
Run real examples locally:
|
|
114
|
+
|
|
115
|
+
### 1. Simple mode (like a normal function)
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
python demo/demo_protect_simple.py
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### 2. Full mode (engineering output)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
python demo/demo_protect_full.py
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### 3. Error handling (simple mode)
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
python demo/demo_error_simple.py
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### 4. Error handling (full mode)
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
python demo/demo_error_full.py
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### 5. Real LLM demo (Ollama)
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
python demo/demo_protect_ollama_simple.py
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
👉 Make sure Ollama is running locally before running this demo.
|
|
154
|
+
|
|
155
|
+
------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
**Logic Fingerprint — Execution Safety Layer (Python)**
|
|
158
|
+
|
|
159
|
+
* Designed and implemented a decorator-based execution control layer for functions, APIs, and LLM calls
|
|
160
|
+
* Built circuit breaker with HALF_OPEN recovery, time-driven probing, and consecutive-success gating
|
|
161
|
+
* Added schema validation (Pydantic) and unified error protocol for stable, observable outputs
|
|
162
|
+
* Delivered dual-mode API (`simple` vs `full`) to support both developer-friendly usage and production observability
|
|
163
|
+
* Integrated with local LLM (Ollama) to demonstrate real-world stability against timeouts and malformed outputs
|
|
164
|
+
* Structured demos and documentation to enable 30-second onboarding and clear behavior comparison
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
Logic Fingerprint — 执行安全层(Python)
|
|
168
|
+
|
|
169
|
+
设计并实现基于装饰器的执行控制层,用于函数 / API / LLM 调用的稳定性保护
|
|
170
|
+
实现熔断机制(CLOSED / OPEN / HALF_OPEN)及时间驱动探测恢复与连续成功判定
|
|
171
|
+
引入输入输出 Schema 校验(Pydantic)与统一错误协议,保证结果结构稳定、可观测
|
|
172
|
+
设计双模式接口(simple / full),兼顾易用性与工程可观测性
|
|
173
|
+
集成本地 LLM(Ollama)进行真实场景验证,解决 timeout、异常输出等问题
|
|
174
|
+
构建分层 demo 与文档体系,实现 30 秒上手与行为对照演示
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
“I built a decorator-based execution safety layer for LLM and API calls.”
|
|
178
|
+
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
|
|
2
|
+
Add a safety layer to any function.
|
|
3
|
+
|
|
4
|
+
Logic Fingerprint protects your function/LLM/API calls with:
|
|
5
|
+
|
|
6
|
+
circuit breaker (no retry storms)
|
|
7
|
+
safe recovery (HALF_OPEN probing)
|
|
8
|
+
schema validation (stable outputs)
|
|
9
|
+
unified error handling
|
|
10
|
+
|
|
11
|
+
Works with a single decorator: @protect()
|
|
12
|
+
|
|
13
|
+
## 🧭 How it works
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
Your Function
|
|
17
|
+
↓
|
|
18
|
+
@protect
|
|
19
|
+
↓
|
|
20
|
+
Logic Fingerprint
|
|
21
|
+
├─ Circuit Breaker
|
|
22
|
+
├─ Probe Recovery
|
|
23
|
+
├─ Validation Layer
|
|
24
|
+
├─ Error Control
|
|
25
|
+
↓
|
|
26
|
+
Safe Execution
|
|
27
|
+
↓
|
|
28
|
+
Result / Error
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## ⚡ Quick Start
|
|
32
|
+
|
|
33
|
+
### Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install -r requirements.txt
|
|
37
|
+
pip install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Protect a real local LLM call
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from logic_fingerprint import protect
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@protect()
|
|
47
|
+
def ask_local_llm(request):
|
|
48
|
+
import json
|
|
49
|
+
import urllib.request
|
|
50
|
+
|
|
51
|
+
payload = {
|
|
52
|
+
"model": "llama3.2",
|
|
53
|
+
"prompt": request.payload["prompt"],
|
|
54
|
+
"stream": False,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
req = urllib.request.Request(
|
|
58
|
+
"http://127.0.0.1:11434/api/generate",
|
|
59
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
60
|
+
headers={"Content-Type": "application/json"},
|
|
61
|
+
method="POST",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
65
|
+
body = json.loads(resp.read().decode("utf-8"))
|
|
66
|
+
|
|
67
|
+
return {"answer": body["response"].strip()}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Call it like a normal function
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
result = ask_local_llm({
|
|
74
|
+
"prompt": "Explain circuit breaker in one sentence."
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
print(result)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Output:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
{'answer': 'A circuit breaker is a safety device that automatically stops an overcurrent flow in an electrical circuit to prevent damage to equipment and potential hazards.'}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### What you get automatically
|
|
87
|
+
|
|
88
|
+
* circuit breaker protection
|
|
89
|
+
* safe recovery probing
|
|
90
|
+
* controlled failure handling
|
|
91
|
+
* optional schema validation
|
|
92
|
+
* auto request context
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
```md
|
|
97
|
+
## 🎬 Demos
|
|
98
|
+
|
|
99
|
+
Run real examples locally:
|
|
100
|
+
|
|
101
|
+
### 1. Simple mode (like a normal function)
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
python demo/demo_protect_simple.py
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### 2. Full mode (engineering output)
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
python demo/demo_protect_full.py
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### 3. Error handling (simple mode)
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python demo/demo_error_simple.py
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### 4. Error handling (full mode)
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
python demo/demo_error_full.py
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### 5. Real LLM demo (Ollama)
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python demo/demo_protect_ollama_simple.py
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
👉 Make sure Ollama is running locally before running this demo.
|
|
140
|
+
|
|
141
|
+
------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
**Logic Fingerprint — Execution Safety Layer (Python)**
|
|
144
|
+
|
|
145
|
+
* Designed and implemented a decorator-based execution control layer for functions, APIs, and LLM calls
|
|
146
|
+
* Built circuit breaker with HALF_OPEN recovery, time-driven probing, and consecutive-success gating
|
|
147
|
+
* Added schema validation (Pydantic) and unified error protocol for stable, observable outputs
|
|
148
|
+
* Delivered dual-mode API (`simple` vs `full`) to support both developer-friendly usage and production observability
|
|
149
|
+
* Integrated with local LLM (Ollama) to demonstrate real-world stability against timeouts and malformed outputs
|
|
150
|
+
* Structured demos and documentation to enable 30-second onboarding and clear behavior comparison
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
Logic Fingerprint — 执行安全层(Python)
|
|
154
|
+
|
|
155
|
+
设计并实现基于装饰器的执行控制层,用于函数 / API / LLM 调用的稳定性保护
|
|
156
|
+
实现熔断机制(CLOSED / OPEN / HALF_OPEN)及时间驱动探测恢复与连续成功判定
|
|
157
|
+
引入输入输出 Schema 校验(Pydantic)与统一错误协议,保证结果结构稳定、可观测
|
|
158
|
+
设计双模式接口(simple / full),兼顾易用性与工程可观测性
|
|
159
|
+
集成本地 LLM(Ollama)进行真实场景验证,解决 timeout、异常输出等问题
|
|
160
|
+
构建分层 demo 与文档体系,实现 30 秒上手与行为对照演示
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
“I built a decorator-based execution safety layer for LLM and API calls.”
|
|
164
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "logic-fingerprint"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Execution safety layer for functions, APIs, and LLMs."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Your Name" }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
license = { text = "MIT" }
|
|
17
|
+
|
|
18
|
+
dependencies = [
|
|
19
|
+
"pydantic>=1.10"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
keywords = [
|
|
23
|
+
"llm",
|
|
24
|
+
"circuit-breaker",
|
|
25
|
+
"middleware",
|
|
26
|
+
"reliability",
|
|
27
|
+
"api",
|
|
28
|
+
"decorator"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
classifiers = [
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"License :: OSI Approved :: MIT License",
|
|
34
|
+
"Operating System :: OS Independent"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from dataclasses import asdict, is_dataclass
|
|
2
|
+
from fastapi import APIRouter, Response
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from .models import HandlerRequest, RequestContext
|
|
5
|
+
from .prometheus_metrics import render_prometheus_metrics
|
|
6
|
+
|
|
7
|
+
class ForceFailRequest(BaseModel):
|
|
8
|
+
reason: str = "MANUAL_FAIL"
|
|
9
|
+
|
|
10
|
+
class ExecuteHandlerContextModel(BaseModel):
|
|
11
|
+
request_id: str | None = None
|
|
12
|
+
trace_id: str | None = None
|
|
13
|
+
user_id: str | None = None
|
|
14
|
+
source: str | None = None
|
|
15
|
+
timestamp: str | None = None
|
|
16
|
+
headers: dict = Field(default_factory=dict)
|
|
17
|
+
metadata: dict = Field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
class ExecuteHandlerRequest(BaseModel):
|
|
20
|
+
handler: str
|
|
21
|
+
payload: dict = Field(default_factory=dict)
|
|
22
|
+
context: ExecuteHandlerContextModel = Field(default_factory=ExecuteHandlerContextModel)
|
|
23
|
+
now: float | None = None
|
|
24
|
+
|
|
25
|
+
def build_router(runtime: object) -> APIRouter:
|
|
26
|
+
router = APIRouter()
|
|
27
|
+
fsm = runtime.fsm
|
|
28
|
+
metrics = runtime.metrics
|
|
29
|
+
|
|
30
|
+
@router.get("/")
|
|
31
|
+
def root():
|
|
32
|
+
return {"service": "logic-fingerprint", "state": fsm.state.value, "docs": "/docs"}
|
|
33
|
+
|
|
34
|
+
@router.get("/favicon.ico")
|
|
35
|
+
def favicon():
|
|
36
|
+
return Response(status_code=204)
|
|
37
|
+
|
|
38
|
+
@router.get("/healthz")
|
|
39
|
+
def healthz():
|
|
40
|
+
return {"ok": True, "state": fsm.state.value}
|
|
41
|
+
|
|
42
|
+
@router.get("/handlers")
|
|
43
|
+
def handlers():
|
|
44
|
+
return {"handlers": runtime.handler_registry.names()}
|
|
45
|
+
|
|
46
|
+
@router.get("/metrics")
|
|
47
|
+
def metrics_view():
|
|
48
|
+
return metrics.snapshot()
|
|
49
|
+
|
|
50
|
+
@router.get("/metrics.prom")
|
|
51
|
+
def metrics_prom():
|
|
52
|
+
return Response(content=render_prometheus_metrics(metrics, fsm), media_type="text/plain; version=0.0.4")
|
|
53
|
+
|
|
54
|
+
@router.post("/force_fail")
|
|
55
|
+
def force_fail(payload: ForceFailRequest):
|
|
56
|
+
fsm.record_hard_fail(payload.reason)
|
|
57
|
+
return {"state": fsm.state.value, "reason": payload.reason}
|
|
58
|
+
|
|
59
|
+
@router.post("/move_half_open")
|
|
60
|
+
def move_half_open():
|
|
61
|
+
fsm.move_to_half_open()
|
|
62
|
+
return {"state": fsm.state.value}
|
|
63
|
+
|
|
64
|
+
@router.post("/execute_handler")
|
|
65
|
+
async def execute_handler(payload: ExecuteHandlerRequest):
|
|
66
|
+
request = HandlerRequest(
|
|
67
|
+
payload=payload.payload,
|
|
68
|
+
context=RequestContext(
|
|
69
|
+
request_id=payload.context.request_id,
|
|
70
|
+
trace_id=payload.context.trace_id,
|
|
71
|
+
user_id=payload.context.user_id,
|
|
72
|
+
source=payload.context.source,
|
|
73
|
+
timestamp=payload.context.timestamp,
|
|
74
|
+
headers=payload.context.headers,
|
|
75
|
+
metadata=payload.context.metadata,
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
outcome = await runtime.middleware.execute_handler_async(payload.handler, request=request, now=payload.now)
|
|
79
|
+
if outcome.succeeded:
|
|
80
|
+
result = outcome.result
|
|
81
|
+
if is_dataclass(result):
|
|
82
|
+
result = asdict(result)
|
|
83
|
+
return {"ok": True, "result": result}
|
|
84
|
+
return {"ok": False, "error": {"code": outcome.error_code, "message": outcome.error_message, "details": outcome.error_details or {}}}
|
|
85
|
+
|
|
86
|
+
return router
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from .api import build_router
|
|
3
|
+
from .lifespan import build_lifespan
|
|
4
|
+
from .runtime import build_runtime
|
|
5
|
+
|
|
6
|
+
def create_app() -> FastAPI:
|
|
7
|
+
runtime = build_runtime()
|
|
8
|
+
app = FastAPI(title="Logic Fingerprint System", version="1.0.0", lifespan=build_lifespan(runtime, interval_seconds=1.0))
|
|
9
|
+
app.state.runtime = runtime
|
|
10
|
+
app.include_router(build_router(runtime))
|
|
11
|
+
return app
|
|
12
|
+
|
|
13
|
+
app = create_app()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class InMemoryConsensusBackend:
|
|
2
|
+
def __init__(self) -> None:
|
|
3
|
+
self._failed_nodes: set[str] = set()
|
|
4
|
+
def mark_failed(self, instance_id: str) -> None:
|
|
5
|
+
self._failed_nodes.add(instance_id)
|
|
6
|
+
def clear_failed(self, instance_id: str) -> None:
|
|
7
|
+
self._failed_nodes.discard(instance_id)
|
|
8
|
+
def fail_count(self) -> int:
|
|
9
|
+
return len(self._failed_nodes)
|
|
10
|
+
def is_failed(self, instance_id: str) -> bool:
|
|
11
|
+
return instance_id in self._failed_nodes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
from .models import HandlerRequest, RequestContext
|
|
4
|
+
|
|
5
|
+
class ContextBuilder:
|
|
6
|
+
def __init__(self, default_source: str = "api") -> None:
|
|
7
|
+
self.default_source = default_source
|
|
8
|
+
def build_context(self, context: RequestContext | None = None) -> RequestContext:
|
|
9
|
+
context = context or RequestContext()
|
|
10
|
+
return RequestContext(
|
|
11
|
+
request_id=context.request_id or f"req-{uuid4().hex}",
|
|
12
|
+
trace_id=context.trace_id or f"trace-{uuid4().hex}",
|
|
13
|
+
user_id=context.user_id,
|
|
14
|
+
source=context.source or self.default_source,
|
|
15
|
+
timestamp=context.timestamp or datetime.now(timezone.utc).isoformat(),
|
|
16
|
+
headers=dict(context.headers),
|
|
17
|
+
metadata=dict(context.metadata),
|
|
18
|
+
)
|
|
19
|
+
def build_request(self, request: HandlerRequest | None = None) -> HandlerRequest:
|
|
20
|
+
request = request or HandlerRequest()
|
|
21
|
+
return HandlerRequest(payload=dict(request.payload), context=self.build_context(request.context))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class ErrorCode(str, Enum):
|
|
4
|
+
ERR_TIMEOUT = "ERR_TIMEOUT"
|
|
5
|
+
ERR_NULL = "ERR_NULL"
|
|
6
|
+
ERR_NORM = "ERR_NORM"
|
|
7
|
+
ERR_LOGIC = "ERR_LOGIC"
|
|
8
|
+
ERR_EXECUTION_BLOCKED = "ERR_EXECUTION_BLOCKED"
|
|
9
|
+
ERR_UNKNOWN = "ERR_UNKNOWN"
|
|
10
|
+
ERR_HANDLER_NOT_FOUND = "ERR_HANDLER_NOT_FOUND"
|
|
11
|
+
ERR_VALIDATION = "ERR_VALIDATION"
|
|
12
|
+
ERR_OUTPUT_VALIDATION = "ERR_OUTPUT_VALIDATION"
|
|
13
|
+
|
|
14
|
+
class LogicFingerprintError(Exception):
|
|
15
|
+
code: ErrorCode = ErrorCode.ERR_UNKNOWN
|
|
16
|
+
def __init__(self, message: str = "", details: dict | None = None) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.message = message or self.__class__.__name__
|
|
19
|
+
self.details = details or {}
|
|
20
|
+
|
|
21
|
+
class TimeoutErrorLF(LogicFingerprintError):
|
|
22
|
+
code = ErrorCode.ERR_TIMEOUT
|
|
23
|
+
|
|
24
|
+
class NullResultError(LogicFingerprintError):
|
|
25
|
+
code = ErrorCode.ERR_NULL
|
|
26
|
+
|
|
27
|
+
class NormalizationError(LogicFingerprintError):
|
|
28
|
+
code = ErrorCode.ERR_NORM
|
|
29
|
+
|
|
30
|
+
class LogicExecutionError(LogicFingerprintError):
|
|
31
|
+
code = ErrorCode.ERR_LOGIC
|
|
32
|
+
|
|
33
|
+
class HandlerNotFoundError(LogicFingerprintError):
|
|
34
|
+
code = ErrorCode.ERR_HANDLER_NOT_FOUND
|
|
35
|
+
|
|
36
|
+
class ValidationErrorLF(LogicFingerprintError):
|
|
37
|
+
code = ErrorCode.ERR_VALIDATION
|
|
38
|
+
|
|
39
|
+
class OutputValidationErrorLF(LogicFingerprintError):
|
|
40
|
+
code = ErrorCode.ERR_OUTPUT_VALIDATION
|
|
41
|
+
|
|
42
|
+
def classify_exception(exc: Exception) -> ErrorCode:
|
|
43
|
+
if isinstance(exc, LogicFingerprintError):
|
|
44
|
+
return exc.code
|
|
45
|
+
if isinstance(exc, TimeoutError):
|
|
46
|
+
return ErrorCode.ERR_TIMEOUT
|
|
47
|
+
if isinstance(exc, ValueError):
|
|
48
|
+
return ErrorCode.ERR_NORM
|
|
49
|
+
return ErrorCode.ERR_UNKNOWN
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from .errors import ErrorCode, NullResultError, classify_exception, LogicFingerprintError
|
|
4
|
+
from .models import ExecutionDecision, ExecutionOutcome, FSMState, ProbeResult
|
|
5
|
+
|
|
6
|
+
@dataclass(slots=True)
|
|
7
|
+
class LogicFingerprintExecutor:
|
|
8
|
+
fsm: object
|
|
9
|
+
|
|
10
|
+
def _build_decision_from_closed(self):
|
|
11
|
+
info = self.fsm.before_request()
|
|
12
|
+
return ExecutionDecision(state=str(info["state"]), allow_request=bool(info["allow_request"]), allow_probe=bool(info["allow_probe"]), global_fail_ratio=float(info["global_fail_ratio"]), external_fail_ratio=float(info["external_fail_ratio"]), is_probe=False)
|
|
13
|
+
def _build_decision_from_half_open(self, now):
|
|
14
|
+
info = self.fsm.before_half_open_request(now=now)
|
|
15
|
+
return ExecutionDecision(state=str(info["state"]), allow_request=bool(info["allow_request"]), allow_probe=bool(info["allow_probe"]), global_fail_ratio=float(info["global_fail_ratio"]), external_fail_ratio=float(info["external_fail_ratio"]), is_probe=bool(info["allow_probe"]))
|
|
16
|
+
def _blocked_outcome(self, decision):
|
|
17
|
+
return ExecutionOutcome(decision=decision, executed=False, succeeded=False, state_after=self.fsm.state.value, error_code=ErrorCode.ERR_EXECUTION_BLOCKED.value, error_message="Request blocked by FSM state.")
|
|
18
|
+
def _success_outcome(self, decision, result):
|
|
19
|
+
if decision.is_probe:
|
|
20
|
+
self.fsm.evaluate_probe(ProbeResult(system_success=True, business_success=True))
|
|
21
|
+
return ExecutionOutcome(decision=decision, executed=True, succeeded=True, state_after=self.fsm.state.value, result=result)
|
|
22
|
+
def _failure_outcome(self, decision, exc):
|
|
23
|
+
code = classify_exception(exc)
|
|
24
|
+
details = exc.details if isinstance(exc, LogicFingerprintError) else {}
|
|
25
|
+
self.fsm.record_hard_fail(code.value)
|
|
26
|
+
return ExecutionOutcome(decision=decision, executed=True, succeeded=False, state_after=self.fsm.state.value, error_code=code.value, error_message=str(exc), error_details=details)
|
|
27
|
+
def execute(self, operation, now=None):
|
|
28
|
+
decision = self._build_decision_from_half_open(now) if self.fsm.state == FSMState.HALF_OPEN else self._build_decision_from_closed()
|
|
29
|
+
if not decision.allow_request and not decision.allow_probe:
|
|
30
|
+
return self._blocked_outcome(decision)
|
|
31
|
+
try:
|
|
32
|
+
result = operation()
|
|
33
|
+
if inspect.isawaitable(result):
|
|
34
|
+
raise TypeError("Async operation cannot be executed by sync execute(); use execute_async().")
|
|
35
|
+
if result is None:
|
|
36
|
+
raise NullResultError("Operation returned None.")
|
|
37
|
+
return self._success_outcome(decision, result)
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
return self._failure_outcome(decision, exc)
|
|
40
|
+
async def execute_async(self, operation, now=None):
|
|
41
|
+
decision = self._build_decision_from_half_open(now) if self.fsm.state == FSMState.HALF_OPEN else self._build_decision_from_closed()
|
|
42
|
+
if not decision.allow_request and not decision.allow_probe:
|
|
43
|
+
return self._blocked_outcome(decision)
|
|
44
|
+
try:
|
|
45
|
+
result = operation()
|
|
46
|
+
if inspect.isawaitable(result):
|
|
47
|
+
result = await result
|
|
48
|
+
if result is None:
|
|
49
|
+
raise NullResultError("Operation returned None.")
|
|
50
|
+
return self._success_outcome(decision, result)
|
|
51
|
+
except Exception as exc:
|
|
52
|
+
return self._failure_outcome(decision, exc)
|