cindyy287 commited on
Commit
32fea32
·
verified ·
1 Parent(s): c2fb337

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +219 -219
app.py CHANGED
@@ -1,219 +1,219 @@
1
- import sys
2
- import os
3
- import time
4
- import json
5
- import logging
6
-
7
- import joblib
8
- import numpy as np
9
- import pandas as pd
10
-
11
- from flask import Flask, request, jsonify
12
- from sklearn.pipeline import Pipeline
13
-
14
- from features.feature_builder import build_features
15
- from schemas.request_schema import PredictRequest
16
-
17
-
18
- # ======================
19
- # PATH SETUP
20
- # ======================
21
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
22
- sys.path.insert(0, BASE_DIR)
23
-
24
-
25
- # ======================
26
- # APP INIT
27
- # ======================
28
- app = Flask(__name__)
29
-
30
-
31
- # ======================
32
- # LOGGING
33
- # ======================
34
- logging.basicConfig(
35
- level=logging.INFO,
36
- format="%(asctime)s - %(levelname)s - %(message)s"
37
- )
38
-
39
-
40
- # ======================
41
- # SECURITY CONFIG
42
- # ======================
43
- API_KEY = os.getenv("FRAUD_API_KEY")
44
- MAX_REQUEST_SIZE = 10_000 # 10 KB
45
- RATE_LIMIT = 30
46
- RATE_LIMIT_WINDOW = 60 # seconds
47
-
48
- rate_limit_store = {}
49
-
50
-
51
- def is_rate_limited(client_ip: str) -> bool:
52
- now = time.time()
53
-
54
- if client_ip not in rate_limit_store:
55
- rate_limit_store[client_ip] = []
56
-
57
- rate_limit_store[client_ip] = [
58
- t for t in rate_limit_store[client_ip]
59
- if now - t < RATE_LIMIT_WINDOW
60
- ]
61
-
62
- if len(rate_limit_store[client_ip]) >= RATE_LIMIT:
63
- return True
64
-
65
- rate_limit_store[client_ip].append(now)
66
- return False
67
-
68
-
69
- # ======================
70
- # GLOBAL API KEY GUARD
71
- # ======================
72
- @app.before_request
73
- def check_api_key():
74
- # Health endpoint is public
75
- if request.path == "/health":
76
- return
77
-
78
- # Skip static files if any
79
- if request.path.startswith("/static"):
80
- return
81
-
82
- if not API_KEY:
83
- logging.error("FRAUD_API_KEY environment variable not set")
84
- return jsonify({"error": "Server misconfigured"}), 500
85
-
86
- client_key = request.headers.get("X-API-KEY")
87
- if not client_key or client_key != API_KEY:
88
- return jsonify({"error": "Unauthorized"}), 401
89
-
90
-
91
- # ======================
92
- # LOAD MODEL & PREPROCESSOR
93
- # ======================
94
- MODEL_PATH = os.path.join(BASE_DIR, "models", "ensemble_model_enhanced.joblib")
95
- PREPROCESSOR_PATH = os.path.join(BASE_DIR, "models", "preprocessor_enhanced.joblib")
96
-
97
- if not os.path.exists(MODEL_PATH):
98
- raise FileNotFoundError(f"Model tidak ditemukan: {MODEL_PATH}")
99
-
100
- if not os.path.exists(PREPROCESSOR_PATH):
101
- raise FileNotFoundError(f"Preprocessor tidak ditemukan: {PREPROCESSOR_PATH}")
102
-
103
- model = joblib.load(MODEL_PATH)
104
- preprocessor = joblib.load(PREPROCESSOR_PATH)
105
-
106
- pipeline_model = Pipeline([
107
- ("preprocess", preprocessor),
108
- ("classifier", model)
109
- ])
110
-
111
- THRESHOLD = 0.6
112
-
113
-
114
- # ======================
115
- # HEALTH CHECK
116
- # ======================
117
- @app.route("/health", methods=["GET"])
118
- def health():
119
- return jsonify({
120
- "status": "ok",
121
- "model_loaded": model is not None,
122
- "timestamp": time.time()
123
- })
124
-
125
-
126
- # ======================
127
- # PREDICT
128
- # ======================
129
- @app.route("/predict", methods=["POST"])
130
- def predict():
131
- start_time = time.time()
132
-
133
- # ---------- REQUEST SIZE ----------
134
- if request.content_length and request.content_length > MAX_REQUEST_SIZE:
135
- return jsonify({"error": "Request too large"}), 413
136
-
137
- # ---------- RATE LIMIT ----------
138
- client_ip = request.remote_addr or "unknown"
139
- if is_rate_limited(client_ip):
140
- return jsonify({"error": "Too many requests"}), 429
141
-
142
- # ---------- PARSE & VALIDATE ----------
143
- try:
144
- payload = request.get_json()
145
- req = PredictRequest(**payload)
146
- data = req.model_dump()
147
- logging.info("Request valid: %s", data)
148
- except Exception as e:
149
- return jsonify({
150
- "error": "Invalid request schema",
151
- "detail": str(e)
152
- }), 422
153
-
154
- # ---------- BUSINESS VALIDATION ----------
155
- amount = data.get("amount", 0)
156
- location = data.get("location", -1)
157
-
158
- if amount <= 0 or amount > 100_000_000:
159
- return jsonify({"error": "Invalid amount value"}), 400
160
-
161
- if location < 0:
162
- return jsonify({"error": "Invalid location value"}), 400
163
-
164
- # ---------- FEATURE BUILD ----------
165
- try:
166
- X_df = build_features(data)
167
- logging.info("Feature DF: %s", X_df.to_dict(orient="records"))
168
- except Exception as e:
169
- logging.error(f"Feature building error: {e}")
170
- return jsonify({
171
- "error": "Feature building error",
172
- "detail": str(e)
173
- }), 500
174
-
175
- # ---------- PREDICT ----------
176
- try:
177
- X = preprocessor.transform(X_df)
178
- fraud_prob = model.predict_proba(X)[0][1]
179
- except Exception as e:
180
- logging.error(f"Prediction error: {e}")
181
- return jsonify({
182
- "error": "Preprocessing or prediction error",
183
- "detail": str(e)
184
- }), 500
185
-
186
- # ---------- DECISION ----------
187
- is_fraud = fraud_prob >= THRESHOLD
188
-
189
- if fraud_prob >= 0.85:
190
- decision = "BLOCK"
191
- elif fraud_prob >= THRESHOLD:
192
- decision = "REVIEW"
193
- else:
194
- decision = "ALLOW"
195
-
196
- latency_ms = round((time.time() - start_time) * 1000, 2)
197
-
198
- # ---------- STRUCTURED LOG ----------
199
- logging.info({
200
- "event": "fraud_decision",
201
- "fraud_probability": float(fraud_prob),
202
- "decision": decision,
203
- "threshold": THRESHOLD,
204
- "latency_ms": latency_ms
205
- })
206
-
207
- return jsonify({
208
- "fraud_probability": round(float(fraud_prob), 4),
209
- "is_fraud": bool(is_fraud),
210
- "decision": decision,
211
- "latency_ms": latency_ms
212
- })
213
-
214
-
215
- # ======================
216
- # RUN SERVER
217
- # ======================
218
- if __name__ == "__main__":
219
- app.run(debug=True, port=5001)
 
1
+ import sys
2
+ import os
3
+ import time
4
+ import json
5
+ import logging
6
+
7
+ import joblib
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ from flask import Flask, request, jsonify
12
+ from sklearn.pipeline import Pipeline
13
+
14
+ from features.feature_builder import build_features
15
+ from schemas.request_schema import PredictRequest
16
+
17
+
18
+ # ======================
19
+ # PATH SETUP
20
+ # ======================
21
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
22
+ sys.path.insert(0, BASE_DIR)
23
+
24
+
25
+ # ======================
26
+ # APP INIT
27
+ # ======================
28
+ app = Flask(__name__)
29
+
30
+
31
+ # ======================
32
+ # LOGGING
33
+ # ======================
34
+ logging.basicConfig(
35
+ level=logging.INFO,
36
+ format="%(asctime)s - %(levelname)s - %(message)s"
37
+ )
38
+
39
+
40
+ # ======================
41
+ # SECURITY CONFIG
42
+ # ======================
43
+ API_KEY = os.getenv("FRAUD_API_KEY")
44
+ MAX_REQUEST_SIZE = 10_000 # 10 KB
45
+ RATE_LIMIT = 30
46
+ RATE_LIMIT_WINDOW = 60 # seconds
47
+
48
+ rate_limit_store = {}
49
+
50
+
51
+ def is_rate_limited(client_ip: str) -> bool:
52
+ now = time.time()
53
+
54
+ if client_ip not in rate_limit_store:
55
+ rate_limit_store[client_ip] = []
56
+
57
+ rate_limit_store[client_ip] = [
58
+ t for t in rate_limit_store[client_ip]
59
+ if now - t < RATE_LIMIT_WINDOW
60
+ ]
61
+
62
+ if len(rate_limit_store[client_ip]) >= RATE_LIMIT:
63
+ return True
64
+
65
+ rate_limit_store[client_ip].append(now)
66
+ return False
67
+
68
+
69
+ # ======================
70
+ # GLOBAL API KEY GUARD
71
+ # ======================
72
+ @app.before_request
73
+ def check_api_key():
74
+ # Health endpoint is public
75
+ # if request.path == "/health":
76
+ # return
77
+
78
+ # # Skip static files if any
79
+ # if request.path.startswith("/static"):
80
+ # return
81
+
82
+ # if not API_KEY:
83
+ # logging.error("FRAUD_API_KEY environment variable not set")
84
+ # return jsonify({"error": "Server misconfigured"}), 500
85
+
86
+ # client_key = request.headers.get("X-API-KEY")
87
+ # if not client_key or client_key != API_KEY:
88
+ # return jsonify({"error": "Unauthorized"}), 401
89
+
90
+
91
+ # ======================
92
+ # LOAD MODEL & PREPROCESSOR
93
+ # ======================
94
+ MODEL_PATH = os.path.join(BASE_DIR, "models", "ensemble_model_enhanced.joblib")
95
+ PREPROCESSOR_PATH = os.path.join(BASE_DIR, "models", "preprocessor_enhanced.joblib")
96
+
97
+ if not os.path.exists(MODEL_PATH):
98
+ raise FileNotFoundError(f"Model tidak ditemukan: {MODEL_PATH}")
99
+
100
+ if not os.path.exists(PREPROCESSOR_PATH):
101
+ raise FileNotFoundError(f"Preprocessor tidak ditemukan: {PREPROCESSOR_PATH}")
102
+
103
+ model = joblib.load(MODEL_PATH)
104
+ preprocessor = joblib.load(PREPROCESSOR_PATH)
105
+
106
+ pipeline_model = Pipeline([
107
+ ("preprocess", preprocessor),
108
+ ("classifier", model)
109
+ ])
110
+
111
+ THRESHOLD = 0.6
112
+
113
+
114
+ # ======================
115
+ # HEALTH CHECK
116
+ # ======================
117
+ @app.route("/health", methods=["GET"])
118
+ def health():
119
+ return jsonify({
120
+ "status": "ok",
121
+ "model_loaded": model is not None,
122
+ "timestamp": time.time()
123
+ })
124
+
125
+
126
+ # ======================
127
+ # PREDICT
128
+ # ======================
129
+ @app.route("/predict", methods=["POST"])
130
+ def predict():
131
+ start_time = time.time()
132
+
133
+ # ---------- REQUEST SIZE ----------
134
+ if request.content_length and request.content_length > MAX_REQUEST_SIZE:
135
+ return jsonify({"error": "Request too large"}), 413
136
+
137
+ # ---------- RATE LIMIT ----------
138
+ client_ip = request.remote_addr or "unknown"
139
+ if is_rate_limited(client_ip):
140
+ return jsonify({"error": "Too many requests"}), 429
141
+
142
+ # ---------- PARSE & VALIDATE ----------
143
+ try:
144
+ payload = request.get_json()
145
+ req = PredictRequest(**payload)
146
+ data = req.model_dump()
147
+ logging.info("Request valid: %s", data)
148
+ except Exception as e:
149
+ return jsonify({
150
+ "error": "Invalid request schema",
151
+ "detail": str(e)
152
+ }), 422
153
+
154
+ # ---------- BUSINESS VALIDATION ----------
155
+ amount = data.get("amount", 0)
156
+ location = data.get("location", -1)
157
+
158
+ if amount <= 0 or amount > 100_000_000:
159
+ return jsonify({"error": "Invalid amount value"}), 400
160
+
161
+ if location < 0:
162
+ return jsonify({"error": "Invalid location value"}), 400
163
+
164
+ # ---------- FEATURE BUILD ----------
165
+ try:
166
+ X_df = build_features(data)
167
+ logging.info("Feature DF: %s", X_df.to_dict(orient="records"))
168
+ except Exception as e:
169
+ logging.error(f"Feature building error: {e}")
170
+ return jsonify({
171
+ "error": "Feature building error",
172
+ "detail": str(e)
173
+ }), 500
174
+
175
+ # ---------- PREDICT ----------
176
+ try:
177
+ X = preprocessor.transform(X_df)
178
+ fraud_prob = model.predict_proba(X)[0][1]
179
+ except Exception as e:
180
+ logging.error(f"Prediction error: {e}")
181
+ return jsonify({
182
+ "error": "Preprocessing or prediction error",
183
+ "detail": str(e)
184
+ }), 500
185
+
186
+ # ---------- DECISION ----------
187
+ is_fraud = fraud_prob >= THRESHOLD
188
+
189
+ if fraud_prob >= 0.85:
190
+ decision = "BLOCK"
191
+ elif fraud_prob >= THRESHOLD:
192
+ decision = "REVIEW"
193
+ else:
194
+ decision = "ALLOW"
195
+
196
+ latency_ms = round((time.time() - start_time) * 1000, 2)
197
+
198
+ # ---------- STRUCTURED LOG ----------
199
+ logging.info({
200
+ "event": "fraud_decision",
201
+ "fraud_probability": float(fraud_prob),
202
+ "decision": decision,
203
+ "threshold": THRESHOLD,
204
+ "latency_ms": latency_ms
205
+ })
206
+
207
+ return jsonify({
208
+ "fraud_probability": round(float(fraud_prob), 4),
209
+ "is_fraud": bool(is_fraud),
210
+ "decision": decision,
211
+ "latency_ms": latency_ms
212
+ })
213
+
214
+
215
+ # ======================
216
+ # RUN SERVER
217
+ # ======================
218
+ if __name__ == "__main__":
219
+ app.run(debug=True, port=5001)