AdarshDRC commited on
Commit
8ab917e
·
verified ·
1 Parent(s): 58c92f2

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +80 -161
main.py CHANGED
@@ -1,6 +1,4 @@
1
-
2
- from contextlib import asynccontextmanager
3
- from collections import OrderedDict
4
  import asyncio
5
  import os
6
  import shutil
@@ -9,28 +7,30 @@ import re
9
  import inflect
10
  from urllib.parse import urlparse
11
  from typing import List
 
 
12
 
13
  from fastapi import FastAPI, UploadFile, File, Form, HTTPException
14
  from fastapi.middleware.cors import CORSMiddleware
15
-
16
  import cloudinary
17
  import cloudinary.uploader
18
  import cloudinary.api
19
  from pinecone import Pinecone, ServerlessSpec
20
 
21
- # ── Deferred imports so startup prints appear in order ────────────
22
- ai = None # set in lifespan
23
- p = inflect.engine()
24
 
25
- # ── Semaphore: max concurrent AI inference jobs ────────────────────
26
  MAX_CONCURRENT_INFERENCES = int(os.getenv("MAX_CONCURRENT_INFERENCES", "6"))
27
  _inference_sem: asyncio.Semaphore
28
 
29
- # ── Simple LRU connection pools ───────────────────────────────────
30
- _pinecone_pool: OrderedDict = OrderedDict()
31
- _cloudinary_pool: dict = {}
32
  _POOL_MAX = 64
33
 
 
 
 
34
 
35
  def _get_pinecone(api_key: str) -> Pinecone:
36
  if api_key not in _pinecone_pool:
@@ -40,279 +40,198 @@ def _get_pinecone(api_key: str) -> Pinecone:
40
  _pinecone_pool.move_to_end(api_key)
41
  return _pinecone_pool[api_key]
42
 
43
-
44
- def _configure_cloudinary(creds: dict) -> None:
45
  key = creds["cloud_name"]
46
  if key not in _cloudinary_pool:
47
  cloudinary.config(
48
- cloud_name=creds["cloud_name"],
49
- api_key=creds["api_key"],
50
- api_secret=creds["api_secret"],
51
  )
52
  _cloudinary_pool[key] = True
53
 
54
-
55
  @asynccontextmanager
56
  async def lifespan(app: FastAPI):
57
  global ai, _inference_sem
58
  from src.models import AIModelManager
59
-
60
  print("⏳ Loading AI models …")
61
  loop = asyncio.get_event_loop()
62
  ai = await loop.run_in_executor(None, AIModelManager)
63
  _inference_sem = asyncio.Semaphore(MAX_CONCURRENT_INFERENCES)
64
- print(f"✅ Ready! Max concurrent inference slots: {MAX_CONCURRENT_INFERENCES}")
65
  yield
66
- print("👋 Shutting down")
67
-
68
 
69
  app = FastAPI(lifespan=lifespan)
70
-
71
- app.add_middleware(
72
- CORSMiddleware,
73
- allow_origins=["*"],
74
- allow_credentials=True,
75
- allow_methods=["*"],
76
- allow_headers=["*"],
77
- )
78
-
79
  os.makedirs("temp_uploads", exist_ok=True)
80
 
81
-
82
  def standardize_category_name(name: str) -> str:
83
  clean = re.sub(r'\s+', '_', name.strip().lower())
84
  clean = re.sub(r'[^\w]', '', clean)
85
  return p.singular_noun(clean) or clean
86
 
87
-
88
  def sanitize_filename(filename: str) -> str:
89
- clean = re.sub(r'\s+', '_', filename)
90
- return re.sub(r'[^\w.\-]', '', clean)
91
-
92
 
93
  def get_cloudinary_creds(env_url: str) -> dict:
94
  parsed = urlparse(env_url)
95
- return {
96
- "api_key": parsed.username,
97
- "api_secret": parsed.password,
98
- "cloud_name": parsed.hostname,
99
- }
100
 
101
  # ══════════════════════════════════════════════════════════════════
102
  # 1. VERIFY KEYS & AUTO-BUILD INDEXES
103
  # ══════════════════════════════════════════════════════════════════
104
  @app.post("/api/verify-keys")
105
- async def verify_keys(
106
- pinecone_key: str = Form(""),
107
- cloudinary_url: str = Form(""),
108
- ):
109
  if cloudinary_url:
110
  try:
111
- creds = get_cloudinary_creds(cloudinary_url)
112
- _configure_cloudinary(creds)
113
  await asyncio.to_thread(cloudinary.api.ping)
114
  except Exception:
115
  raise HTTPException(400, "Invalid Cloudinary Environment URL.")
116
-
117
  if pinecone_key:
118
  try:
119
  pc = _get_pinecone(pinecone_key)
120
  existing = {idx.name for idx in await asyncio.to_thread(pc.list_indexes)}
121
-
122
  tasks = []
123
- if "lens-objects" not in existing:
124
- tasks.append(asyncio.to_thread(
125
- pc.create_index,
126
- name="lens-objects", dimension=1536, metric="cosine",
127
- spec=ServerlessSpec(cloud="aws", region="us-east-1"),
128
- ))
129
- if "lens-faces" not in existing:
130
- tasks.append(asyncio.to_thread(
131
- pc.create_index,
132
- name="lens-faces", dimension=512, metric="cosine",
133
- spec=ServerlessSpec(cloud="aws", region="us-east-1"),
134
- ))
135
-
136
  if tasks:
137
  await asyncio.gather(*tasks)
138
-
139
- except HTTPException:
140
- raise
141
  except Exception as e:
142
  raise HTTPException(400, f"Pinecone Error: {e}")
143
-
144
  return {"message": "Keys verified and indexes ready!"}
145
 
146
 
147
  # ══════════════════════════════════════════════════════════════════
148
- # 2. UPLOAD (With Demo Fallback)
149
  # ══════════════════════════════════════════════════════════════════
150
  @app.post("/api/upload")
151
- async def upload_new_images(
152
- files: List[UploadFile] = File(...),
153
- folder_name: str = Form(...),
154
- detect_faces: bool = Form(True),
155
- user_pinecone_key: str = Form(""),
156
- user_cloudinary_url: str = Form(""),
157
- ):
158
- # FALLBACK LOGIC: Use user keys if provided, otherwise use Space secrets
159
  actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
160
  actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
161
-
162
  if not actual_pc_key or not actual_cld_url:
163
- raise HTTPException(status_code=400, detail="Cloudinary URL and Pinecone API Key are required.")
164
 
165
- folder = standardize_category_name(folder_name)
166
  uploaded_urls = []
167
-
168
- cld_creds = get_cloudinary_creds(actual_cld_url)
169
- _configure_cloudinary(cld_creds)
170
- pc = _get_pinecone(actual_pc_key)
171
- idx_obj = pc.Index("lens-objects")
172
- idx_face = pc.Index("lens-faces")
173
 
174
  for file in files:
175
- safe_name = sanitize_filename(file.filename)
176
- tmp_path = f"temp_uploads/{uuid.uuid4().hex}_{safe_name}"
177
-
178
  try:
179
  with open(tmp_path, "wb") as buf:
180
  shutil.copyfileobj(file.file, buf)
181
-
182
- # Upload image to CDN
183
- result = await asyncio.to_thread(cloudinary.uploader.upload, tmp_path, folder=folder)
184
- image_url = result["secure_url"]
185
  uploaded_urls.append(image_url)
186
 
187
- # AI inference
188
  async with _inference_sem:
189
  vectors = await ai.process_image_async(tmp_path, is_query=False, detect_faces=detect_faces)
190
 
191
- # Save vectors
192
- face_upserts = []
193
- object_upserts = []
194
-
195
  for v in vectors:
196
  vec_list = v["vector"].tolist() if hasattr(v["vector"], "tolist") else v["vector"]
197
- record = {
198
- "id": str(uuid.uuid4()),
199
- "values": vec_list,
200
- "metadata": {"url": image_url, "folder": folder},
201
- }
202
  (face_upserts if v["type"] == "face" else object_upserts).append(record)
203
 
204
  upsert_tasks = []
205
- if face_upserts:
206
- upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
207
- if object_upserts:
208
- upsert_tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=object_upserts))
209
- if upsert_tasks:
210
- await asyncio.gather(*upsert_tasks)
211
-
212
  except Exception as e:
213
- print(f"❌ Upload error for {file.filename}: {e}")
214
  finally:
215
- if os.path.exists(tmp_path):
216
- os.remove(tmp_path)
217
-
218
  return {"message": "Done!", "urls": uploaded_urls}
219
 
220
 
221
  # ══════════════════════════════════════════════════════════════════
222
- # 3. SEARCH (With Demo Fallback)
223
  # ══════════════════════════════════════════════════════════════════
224
  @app.post("/api/search")
225
- async def search_database(
226
- file: UploadFile = File(...),
227
- detect_faces: bool = Form(True),
228
- user_pinecone_key: str = Form(""),
229
- user_cloudinary_url: str = Form(""),
230
- ):
231
  actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
232
-
233
  if not actual_pc_key:
234
- raise HTTPException(status_code=400, detail="Pinecone API Key is required to search.")
235
-
236
- safe_name = sanitize_filename(file.filename)
237
- tmp_path = f"temp_uploads/query_{uuid.uuid4().hex}_{safe_name}"
238
 
 
239
  try:
240
  with open(tmp_path, "wb") as buf:
241
  shutil.copyfileobj(file.file, buf)
242
 
243
- # AI inference
244
  async with _inference_sem:
245
  vectors = await ai.process_image_async(tmp_path, is_query=True, detect_faces=detect_faces)
246
 
247
- pc = _get_pinecone(actual_pc_key)
248
- idx_obj = pc.Index("lens-objects")
249
- idx_face = pc.Index("lens-faces")
250
 
251
- async def _query_one(vec_dict: dict) -> list[dict]:
252
- vec_list = (vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"])
253
  target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
254
- res = await asyncio.to_thread(
255
- target_idx.query,
256
- vector=vec_list, top_k=10, include_metadata=True,
257
- )
 
 
 
 
 
258
  out = []
259
  for match in res.get("matches", []):
260
- caption = ("👤 Verified Identity" if vec_dict["type"] == "face" else match["metadata"].get("folder", "🎯 Object Match"))
261
- out.append({
262
- "url": match["metadata"].get("url", ""),
263
- "score": match["score"],
264
- "caption": caption,
265
- })
266
  return out
267
 
268
  nested = await asyncio.gather(*[_query_one(v) for v in vectors])
269
  all_results = [r for sub in nested for r in sub]
270
 
271
- seen: dict[str, dict] = {}
272
  for r in all_results:
273
  url = r["url"]
274
  if url not in seen or r["score"] > seen[url]["score"]:
275
  seen[url] = r
276
 
277
- final = sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:10]
278
- return {"results": final}
279
-
280
  except Exception as e:
281
  print(f"❌ Search error: {e}")
282
  raise HTTPException(500, str(e))
283
  finally:
284
- if os.path.exists(tmp_path):
285
- os.remove(tmp_path)
286
 
287
 
288
  # ══════════════════════════════════════════════════════════════════
289
- # 4. CATEGORIES (With Demo Fallback)
290
  # ══════════════════════════════════════════════════════════════════
291
  @app.post("/api/categories")
292
  async def get_categories(user_cloudinary_url: str = Form("")):
293
  actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
294
-
295
  if not actual_cld_url:
296
  return {"categories": []}
297
-
298
  try:
299
- creds = get_cloudinary_creds(actual_cld_url)
300
- _configure_cloudinary(creds)
301
- result = await asyncio.to_thread(cloudinary.api.root_folders)
302
- folders = [f["name"] for f in result.get("folders", [])]
303
- return {"categories": folders}
304
  except Exception as e:
305
  print(f"Category fetch error: {e}")
306
  return {"categories": []}
307
 
308
 
309
- # ══════════════════════════════════════════════════════════════════
310
- # 5. HEALTH CHECK
311
- # ══════════════════════════════════════════════════════════════════
312
  @app.get("/api/health")
313
  async def health():
314
- return {
315
- "status": "ok",
316
- "device": ai.device if ai else "loading",
317
- "sem_slots": _inference_sem._value if _inference_sem else 0,
318
- }
 
1
+ # main.py
 
 
2
  import asyncio
3
  import os
4
  import shutil
 
7
  import inflect
8
  from urllib.parse import urlparse
9
  from typing import List
10
+ from contextlib import asynccontextmanager
11
+ from collections import OrderedDict
12
 
13
  from fastapi import FastAPI, UploadFile, File, Form, HTTPException
14
  from fastapi.middleware.cors import CORSMiddleware
 
15
  import cloudinary
16
  import cloudinary.uploader
17
  import cloudinary.api
18
  from pinecone import Pinecone, ServerlessSpec
19
 
20
+ # ── Deferred imports ─────────────────────────────────────────────
21
+ ai = None
22
+ p = inflect.engine()
23
 
 
24
  MAX_CONCURRENT_INFERENCES = int(os.getenv("MAX_CONCURRENT_INFERENCES", "6"))
25
  _inference_sem: asyncio.Semaphore
26
 
27
+ _pinecone_pool = OrderedDict()
28
+ _cloudinary_pool = {}
 
29
  _POOL_MAX = 64
30
 
31
+ # FIX 1: Restored your original Pinecone Index names!
32
+ IDX_FACES = "enterprise-faces"
33
+ IDX_OBJECTS = "enterprise-objects"
34
 
35
  def _get_pinecone(api_key: str) -> Pinecone:
36
  if api_key not in _pinecone_pool:
 
40
  _pinecone_pool.move_to_end(api_key)
41
  return _pinecone_pool[api_key]
42
 
43
+ def _configure_cloudinary(creds: dict):
 
44
  key = creds["cloud_name"]
45
  if key not in _cloudinary_pool:
46
  cloudinary.config(
47
+ cloud_name=creds["cloud_name"],
48
+ api_key=creds["api_key"],
49
+ api_secret=creds["api_secret"]
50
  )
51
  _cloudinary_pool[key] = True
52
 
 
53
  @asynccontextmanager
54
  async def lifespan(app: FastAPI):
55
  global ai, _inference_sem
56
  from src.models import AIModelManager
57
+
58
  print("⏳ Loading AI models …")
59
  loop = asyncio.get_event_loop()
60
  ai = await loop.run_in_executor(None, AIModelManager)
61
  _inference_sem = asyncio.Semaphore(MAX_CONCURRENT_INFERENCES)
62
+ print("✅ Ready!")
63
  yield
 
 
64
 
65
  app = FastAPI(lifespan=lifespan)
66
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
 
 
 
67
  os.makedirs("temp_uploads", exist_ok=True)
68
 
 
69
  def standardize_category_name(name: str) -> str:
70
  clean = re.sub(r'\s+', '_', name.strip().lower())
71
  clean = re.sub(r'[^\w]', '', clean)
72
  return p.singular_noun(clean) or clean
73
 
 
74
  def sanitize_filename(filename: str) -> str:
75
+ return re.sub(r'[^\w.\-]', '', re.sub(r'\s+', '_', filename))
 
 
76
 
77
  def get_cloudinary_creds(env_url: str) -> dict:
78
  parsed = urlparse(env_url)
79
+ return {"api_key": parsed.username, "api_secret": parsed.password, "cloud_name": parsed.hostname}
 
 
 
 
80
 
81
  # ══════════════════════════════════════════════════════════════════
82
  # 1. VERIFY KEYS & AUTO-BUILD INDEXES
83
  # ══════════════════════════════════════════════════════════════════
84
  @app.post("/api/verify-keys")
85
+ async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
 
 
 
86
  if cloudinary_url:
87
  try:
88
+ _configure_cloudinary(get_cloudinary_creds(cloudinary_url))
 
89
  await asyncio.to_thread(cloudinary.api.ping)
90
  except Exception:
91
  raise HTTPException(400, "Invalid Cloudinary Environment URL.")
 
92
  if pinecone_key:
93
  try:
94
  pc = _get_pinecone(pinecone_key)
95
  existing = {idx.name for idx in await asyncio.to_thread(pc.list_indexes)}
 
96
  tasks = []
97
+ if IDX_OBJECTS not in existing:
98
+ tasks.append(asyncio.to_thread(pc.create_index, name=IDX_OBJECTS, dimension=1536, metric="cosine", spec=ServerlessSpec(cloud="aws", region="us-east-1")))
99
+ if IDX_FACES not in existing:
100
+ tasks.append(asyncio.to_thread(pc.create_index, name=IDX_FACES, dimension=512, metric="cosine", spec=ServerlessSpec(cloud="aws", region="us-east-1")))
 
 
 
 
 
 
 
 
 
101
  if tasks:
102
  await asyncio.gather(*tasks)
 
 
 
103
  except Exception as e:
104
  raise HTTPException(400, f"Pinecone Error: {e}")
 
105
  return {"message": "Keys verified and indexes ready!"}
106
 
107
 
108
  # ══════════════════════════════════════════════════════════════════
109
+ # 2. UPLOAD (Strictly Cloud-Native + Freemium Default)
110
  # ══════════════════════════════════════════════════════════════════
111
  @app.post("/api/upload")
112
+ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: str = Form(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
113
+ # FIX 2: Uses user keys if provided, otherwise falls back to HF Secrets
 
 
 
 
 
 
114
  actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
115
  actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
116
+
117
  if not actual_pc_key or not actual_cld_url:
118
+ raise HTTPException(400, "API Keys are required. Set defaults in HF Secrets.")
119
 
120
+ folder = standardize_category_name(folder_name)
121
  uploaded_urls = []
122
+ _configure_cloudinary(get_cloudinary_creds(actual_cld_url))
123
+ pc = _get_pinecone(actual_pc_key)
124
+ idx_obj = pc.Index(IDX_OBJECTS)
125
+ idx_face = pc.Index(IDX_FACES)
 
 
126
 
127
  for file in files:
128
+ tmp_path = f"temp_uploads/{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
 
 
129
  try:
130
  with open(tmp_path, "wb") as buf:
131
  shutil.copyfileobj(file.file, buf)
132
+
133
+ res = await asyncio.to_thread(cloudinary.uploader.upload, tmp_path, folder=folder)
134
+ image_url = res["secure_url"]
 
135
  uploaded_urls.append(image_url)
136
 
 
137
  async with _inference_sem:
138
  vectors = await ai.process_image_async(tmp_path, is_query=False, detect_faces=detect_faces)
139
 
140
+ face_upserts, object_upserts = [], []
 
 
 
141
  for v in vectors:
142
  vec_list = v["vector"].tolist() if hasattr(v["vector"], "tolist") else v["vector"]
143
+ record = {"id": str(uuid.uuid4()), "values": vec_list, "metadata": {"url": image_url, "folder": folder}}
 
 
 
 
144
  (face_upserts if v["type"] == "face" else object_upserts).append(record)
145
 
146
  upsert_tasks = []
147
+ if face_upserts: upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
148
+ if object_upserts: upsert_tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=object_upserts))
149
+ if upsert_tasks: await asyncio.gather(*upsert_tasks)
 
 
 
 
150
  except Exception as e:
151
+ print(f"❌ Upload error: {e}")
152
  finally:
153
+ if os.path.exists(tmp_path): os.remove(tmp_path)
154
+
 
155
  return {"message": "Done!", "urls": uploaded_urls}
156
 
157
 
158
  # ══════════════════════════════════════════════════════════════════
159
+ # 3. SEARCH (Strictly Cloud-Native + Freemium Default)
160
  # ══════════════════════════════════════════════════════════════════
161
  @app.post("/api/search")
162
+ async def search_database(file: UploadFile = File(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
 
 
 
 
 
163
  actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
 
164
  if not actual_pc_key:
165
+ raise HTTPException(400, "Pinecone Key is required.")
 
 
 
166
 
167
+ tmp_path = f"temp_uploads/query_{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
168
  try:
169
  with open(tmp_path, "wb") as buf:
170
  shutil.copyfileobj(file.file, buf)
171
 
 
172
  async with _inference_sem:
173
  vectors = await ai.process_image_async(tmp_path, is_query=True, detect_faces=detect_faces)
174
 
175
+ pc = _get_pinecone(actual_pc_key)
176
+ idx_obj = pc.Index(IDX_OBJECTS)
177
+ idx_face = pc.Index(IDX_FACES)
178
 
179
+ async def _query_one(vec_dict: dict):
180
+ vec_list = vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"]
181
  target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
182
+
183
+ try:
184
+ res = await asyncio.to_thread(target_idx.query, vector=vec_list, top_k=10, include_metadata=True)
185
+ except Exception as e:
186
+ if "404" in str(e):
187
+ # Graceful error if index truly doesn't exist
188
+ raise HTTPException(404, f"Pinecone Index not found. Please log in and click 'Verify Keys' in Settings to build the indexes.")
189
+ raise e
190
+
191
  out = []
192
  for match in res.get("matches", []):
193
+ caption = "👤 Verified Identity" if vec_dict["type"] == "face" else match["metadata"].get("folder", "🎯 Object Match")
194
+ out.append({"url": match["metadata"].get("url", ""), "score": match["score"], "caption": caption})
 
 
 
 
195
  return out
196
 
197
  nested = await asyncio.gather(*[_query_one(v) for v in vectors])
198
  all_results = [r for sub in nested for r in sub]
199
 
200
+ seen = {}
201
  for r in all_results:
202
  url = r["url"]
203
  if url not in seen or r["score"] > seen[url]["score"]:
204
  seen[url] = r
205
 
206
+ return {"results": sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:10]}
207
+ except HTTPException:
208
+ raise
209
  except Exception as e:
210
  print(f"❌ Search error: {e}")
211
  raise HTTPException(500, str(e))
212
  finally:
213
+ if os.path.exists(tmp_path): os.remove(tmp_path)
 
214
 
215
 
216
  # ══════════════════════════════════════════════════════════════════
217
+ # 4. CATEGORIES (Strictly Cloud-Native + Freemium Default)
218
  # ══════════════════════════════════════════════════════════════════
219
  @app.post("/api/categories")
220
  async def get_categories(user_cloudinary_url: str = Form("")):
221
  actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
 
222
  if not actual_cld_url:
223
  return {"categories": []}
224
+
225
  try:
226
+ # FIX 3: Removed local folder scanning logic. Guests now strictly see Cloudinary folders.
227
+ _configure_cloudinary(get_cloudinary_creds(actual_cld_url))
228
+ result = await asyncio.to_thread(cloudinary.api.root_folders)
229
+ return {"categories": [f["name"] for f in result.get("folders", [])]}
 
230
  except Exception as e:
231
  print(f"Category fetch error: {e}")
232
  return {"categories": []}
233
 
234
 
 
 
 
235
  @app.get("/api/health")
236
  async def health():
237
+ return {"status": "ok"}