rairo commited on
Commit
4ee73f4
·
verified ·
1 Parent(s): 1e10a1a

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +1 -464
main.py CHANGED
@@ -20,7 +20,6 @@ app = Flask(__name__)
20
  CORS(app)
21
 
22
  # ---------- Firebase initialization ----------
23
- Firebase_DB = os.getenv("Firebase_DB")
24
  Firebase_Storage = os.getenv("Firebase_Storage")
25
 
26
  try:
@@ -28,7 +27,6 @@ try:
28
  if cred_json:
29
  cred = credentials.Certificate(json.loads(cred_json))
30
  firebase_admin.initialize_app(cred, {
31
- 'databaseURL': Firebase_DB,
32
  'storageBucket': Firebase_Storage
33
  })
34
  else:
@@ -40,470 +38,9 @@ bucket = storage.bucket()
40
 
41
  # ---------- Helper functions ----------
42
  def configure_gemini():
43
- genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
44
  return genai.GenerativeModel('gemini-2.0-flash-thinking-exp')
45
 
46
- def verify_token(token):
47
- try:
48
- return auth.verify_id_token(token)['uid']
49
- except:
50
- return None
51
 
52
- def verify_global_admin(header):
53
- if not header or not header.startswith('Bearer '):
54
- raise PermissionError('Invalid token')
55
- uid = verify_token(header.split(' ')[1])
56
- if not uid or not db.reference(f'users/{uid}').get().get('is_admin', False):
57
- raise PermissionError('Global admin required')
58
- return uid
59
-
60
- def verify_org_manager(uid, org_id):
61
- org = db.reference(f'organizations/{org_id}').get() or {}
62
- if uid != org.get('owner') and uid not in org.get('managers', []):
63
- raise PermissionError('Manager access required')
64
- return org
65
-
66
- def get_auth_uid():
67
- h = request.headers.get('Authorization', '')
68
- parts = h.split(' ')
69
- return verify_token(parts[1]) if len(parts) == 2 and parts[0]=='Bearer' else None
70
-
71
- def get_blob_path(url):
72
- p = urlparse(url)
73
- if p.netloc == "storage.googleapis.com":
74
- return '/'.join(p.path.lstrip('/').split('/')[1:])
75
- prefix = f"/v0/b/{bucket.name}/o/"
76
- if p.path.startswith(prefix):
77
- return unquote(p.path[len(prefix):])
78
- return None
79
-
80
- # ========================================
81
- # AUTH & REGISTRATION
82
- # ========================================
83
- @app.route('/api/register', methods=['POST'])
84
- def register():
85
- data = request.get_json() or {}
86
- email, password = data.get('email'), data.get('password')
87
- if not email or not password:
88
- return jsonify(error='email and password required'), 400
89
- try:
90
- user = auth.create_user(email=email, password=password)
91
- db.reference(f'users/{user.uid}').set({
92
- 'daily_cash': 0,
93
- 'remaining_cash': 0,
94
- 'last_reset': datetime.now(pytz.UTC).isoformat(),
95
- 'is_admin': False
96
- })
97
- return jsonify(success=True, uid=user.uid, email=user.email), 201
98
- except Exception as e:
99
- return jsonify(error=str(e)), 400
100
-
101
- # ========================================
102
- # ORGANIZATIONS & INVITES
103
- # ========================================
104
- @app.route('/api/orgs', methods=['POST'])
105
- def create_org():
106
- uid = get_auth_uid()
107
- if not uid: return jsonify(error='Invalid token'), 401
108
- name = (request.get_json() or {}).get('name')
109
- if not name: return jsonify(error='Organization name required'), 400
110
- ref = db.reference('organizations').push({
111
- 'name': name,
112
- 'owner': uid,
113
- 'managers': [],
114
- 'members': [uid],
115
- 'created_at': datetime.now(pytz.UTC).isoformat()
116
- })
117
- return jsonify(success=True, org_id=ref.key), 201
118
-
119
- @app.route('/api/orgs', methods=['GET'])
120
- def list_orgs():
121
- uid = get_auth_uid()
122
- if not uid: return jsonify(error='Invalid token'), 401
123
- all_orgs = db.reference('organizations').get() or {}
124
- mine = [{'org_id': oid, **o} for oid,o in all_orgs.items() if uid in o.get('members',[])]
125
- return jsonify(organizations=mine)
126
-
127
- @app.route('/api/orgs/<org_id>/invite', methods=['POST'])
128
- def invite_user(org_id):
129
- uid = get_auth_uid()
130
- try: verify_org_manager(uid, org_id)
131
- except PermissionError as e: return jsonify(error=str(e)), 403
132
- j = request.get_json() or {}
133
- email, role = j.get('email'), j.get('role','member')
134
- if role not in ('member','manager'): return jsonify(error='invalid role'), 400
135
- invite_id = str(uuid.uuid4())
136
- db.reference(f'invites/{invite_id}').set({
137
- 'org_id': org_id,
138
- 'email': email,
139
- 'role': role,
140
- 'sent_at': datetime.now(pytz.UTC).isoformat()
141
- })
142
- return jsonify(success=True, invite_id=invite_id)
143
-
144
- @app.route('/api/orgs/invite/accept', methods=['POST'])
145
- def accept_invite():
146
- uid = get_auth_uid()
147
- if not uid: return jsonify(error='Invalid token'), 401
148
- inv = db.reference(f'invites/{(request.get_json() or {}).get("invite_id")}').get()
149
- if not inv: return jsonify(error='Invite not found'), 404
150
- if auth.get_user(uid).email.lower() != inv['email'].lower():
151
- return jsonify(error='Email mismatch'), 403
152
- org_ref = db.reference(f'organizations/{inv["org_id"]}')
153
- org = org_ref.get() or {}
154
- m, g = set(org.get('members',[])), set(org.get('managers',[]))
155
- m.add(uid)
156
- if inv['role']=='manager': g.add(uid)
157
- org_ref.update({'members':list(m), 'managers':list(g)})
158
- db.reference(f'invites/{inv["invite_id"]}').delete()
159
- return jsonify(success=True, org_id=inv['org_id'])
160
-
161
- # ========================================
162
- # PROJECTS & ALLOCATIONS
163
- # ========================================
164
- @app.route('/api/orgs/<org_id>/projects', methods=['POST'])
165
- def create_project(org_id):
166
- uid = get_auth_uid()
167
- try: verify_org_manager(uid, org_id)
168
- except PermissionError as e: return jsonify(error=str(e)), 403
169
- b = request.get_json() or {}
170
- name = b.get('name'); budget = float(b.get('budget',0))
171
- if not name or budget<=0: return jsonify(error='name and positive budget required'), 400
172
- ref = db.reference('projects').push({
173
- 'org_id': org_id,
174
- 'name': name,
175
- 'budget': budget,
176
- 'spent': 0.0,
177
- 'recurring': bool(b.get('recurring',False)),
178
- 'interval': b.get('interval'),
179
- 'due_date': b.get('due_date'),
180
- 'allocations': {},
181
- 'created_at': datetime.now(pytz.UTC).isoformat()
182
- })
183
- return jsonify(success=True, project_id=ref.key), 201
184
-
185
- @app.route('/api/orgs/<org_id>/projects', methods=['GET'])
186
- def list_projects(org_id):
187
- uid = get_auth_uid()
188
- org = db.reference(f'organizations/{org_id}').get() or {}
189
- if uid not in org.get('members',[]): return jsonify(error='Access denied'), 403
190
- projs = db.reference('projects').order_by_child('org_id').equal_to(org_id).get() or {}
191
- return jsonify(projects=[{'project_id':k,**v} for k,v in projs.items()])
192
-
193
- @app.route('/api/orgs/<org_id>/projects/<pid>', methods=['PUT','DELETE'])
194
- def modify_project(org_id, pid):
195
- uid = get_auth_uid()
196
- try: verify_org_manager(uid, org_id)
197
- except PermissionError as e: return jsonify(error=str(e)), 403
198
- ref = db.reference(f'projects/{pid}')
199
- if request.method=='PUT':
200
- ref.update(request.get_json() or {})
201
- else:
202
- ref.delete()
203
- return jsonify(success=True)
204
-
205
- # Allocations
206
- @app.route('/api/orgs/<org_id>/projects/<pid>/allocations', methods=['POST','GET'])
207
- def allocations(pid, org_id):
208
- uid = get_auth_uid()
209
- org = db.reference(f'organizations/{org_id}').get() or {}
210
- if request.method=='POST':
211
- try: verify_org_manager(uid, org_id)
212
- except PermissionError as e: return jsonify(error=str(e)), 403
213
- b = request.get_json() or {}
214
- name, amt = b.get('name'), float(b.get('budget',0))
215
- if not name or amt<=0: return jsonify(error='name and positive budget required'), 400
216
- aid = str(uuid.uuid4())
217
- db.reference(f'projects/{pid}/allocations/{aid}').set({
218
- 'name': name, 'budget': amt, 'spent': 0.0
219
- })
220
- return jsonify(success=True, allocation_id=aid), 201
221
- # GET
222
- if uid not in org.get('members',[]): return jsonify(error='Access denied'), 403
223
- allocs = db.reference(f'projects/{pid}/allocations').get() or {}
224
- return jsonify(allocations=[{'allocation_id':k,**v} for k,v in allocs.items()])
225
-
226
- @app.route('/api/orgs/<org_id>/projects/<pid>/allocations/<aid>', methods=['PUT','DELETE'])
227
- def modify_allocation(org_id, pid, aid):
228
- uid = get_auth_uid()
229
- try: verify_org_manager(uid, org_id)
230
- except PermissionError as e: return jsonify(error=str(e)), 403
231
- ref = db.reference(f'projects/{pid}/allocations/{aid}')
232
- if request.method=='PUT':
233
- updates = {}
234
- j = request.get_json() or {}
235
- if 'name' in j: updates['name'] = j['name']
236
- if 'budget' in j: updates['budget'] = float(j['budget'])
237
- if not updates: return jsonify(error='nothing to update'), 400
238
- ref.update(updates)
239
- else:
240
- ref.delete()
241
- return jsonify(success=True)
242
-
243
- # ========================================
244
- # RECEIPTS, MANUAL ENTRY, AI REPORT
245
- # ========================================
246
- def process_receipt(model, image):
247
- prompt = """Analyze receipt... return JSON with keys: is_receipt,total,items,date,receipt_number"""
248
- try:
249
- return model.generate_content([prompt, image]).text
250
- except:
251
- return "{}"
252
-
253
- @app.route('/api/process-receipt', methods=['POST'])
254
- def process_receipt_endpoint():
255
- uid = get_auth_uid()
256
- if not uid: return jsonify(error='Invalid token'), 401
257
-
258
- # Confirmation branch
259
- if request.form.get('confirmed')=='true':
260
- data = {**{k:request.form[k] for k in ('date','receipt_number','image_url')},
261
- 'total':float(request.form['total']),
262
- 'items': [i.strip() for i in request.form['items'].split(',')],
263
- }
264
- return validate_and_save_transaction(uid, db.reference(f'users/{uid}').get(), data,
265
- request.form['file_hash'], None, False)
266
-
267
- file = request.files.get('receipt')
268
- if not file: return jsonify(error='No file uploaded'), 400
269
- img_bytes = file.read()
270
- file_hash = hashlib.md5(img_bytes).hexdigest()
271
- if db.reference('transactions').order_by_child('hash').equal_to(file_hash).get():
272
- return jsonify(error='Receipt already processed'), 400
273
-
274
- ts = datetime.now().strftime('%Y%m%d_%H%M%S')
275
- blob = bucket.blob(f'receipts/{uid}/{ts}_{file_hash}.jpg')
276
- blob.upload_from_string(img_bytes, content_type='image/jpeg')
277
- img = Image.open(io.BytesIO(img_bytes))
278
- buf = io.BytesIO(); img.save(buf, 'JPEG', optimize=True, quality=90)
279
- text = process_receipt(configure_gemini(), Image.open(io.BytesIO(buf.getvalue())))
280
- try:
281
- js = json.loads(text[text.find('{'):text.rfind('}')+1])
282
- except:
283
- return jsonify(error='Parse error', raw=text), 400
284
- if not js.get('is_receipt'): return jsonify(error='Not a valid receipt'), 400
285
-
286
- js.update(file_hash=file_hash, image_url=blob.public_url)
287
- return jsonify(success=True, extracted=True, data=js, message='Confirm to save')
288
-
289
- @app.route('/api/manual-entry', methods=['POST'])
290
- def manual_entry():
291
- uid = get_auth_uid()
292
- if not uid: return jsonify(error='Invalid token'), 401
293
- user = db.reference(f'users/{uid}')
294
- data = {
295
- 'total': float(request.form.get('total',0)),
296
- 'items': [i.strip() for i in request.form.get('items','').split(',')],
297
- 'date': request.form.get('date'),
298
- 'receipt_number': request.form.get('receipt_number')
299
- }
300
- return validate_and_save_transaction(uid, user.get(), data,
301
- hashlib.md5(str(datetime.now()).encode()).hexdigest(),
302
- None, True)
303
-
304
- def validate_and_save_transaction(uid, user_data, data, file_hash, img_bytes, manual):
305
- total = float(data.get('total',0))
306
- db.reference(f'users/{uid}').update({
307
- 'remaining_cash': user_data['remaining_cash'] - total
308
- })
309
-
310
- # project/allocation spend
311
- pid = data.get('project_id'); aid = data.get('allocation_id')
312
- if pid:
313
- proj_ref = db.reference(f'projects/{pid}')
314
- p = proj_ref.get() or {}
315
- proj_ref.update({'spent': p.get('spent',0)+total})
316
- if aid:
317
- alloc_ref = proj_ref.child(f'allocations/{aid}')
318
- a = alloc_ref.get() or {}
319
- alloc_ref.update({'spent': a.get('spent',0)+total})
320
-
321
- tx = {
322
- 'uid': uid, 'total': total, 'items': data.get('items',[]),
323
- 'date': data.get('date'), 'receipt_number': data.get('receipt_number'),
324
- 'timestamp': datetime.now(pytz.UTC).isoformat(),
325
- 'hash': file_hash, 'manual_entry': manual,
326
- 'project_id': pid, 'allocation_id': aid
327
- }
328
- if img_bytes:
329
- ts = datetime.now().strftime('%Y%m%d_%H%M%S')
330
- blob = bucket.blob(f'receipts/{uid}/{ts}_{file_hash}.jpg')
331
- blob.upload_from_string(img_bytes, 'image/jpeg')
332
- tx['image_url'] = blob.public_url
333
-
334
- new = db.reference('transactions').push(tx)
335
- return jsonify(success=True, transaction={**tx,'id':new.key})
336
-
337
- # ========================================
338
- # PERSONAL OVERVIEW & PROFILE
339
- # ========================================
340
- @app.route('/api/user/spending-overview', methods=['GET'])
341
- def spending_overview():
342
- uid = get_auth_uid()
343
- ref = db.reference('transactions').order_by_child('uid').equal_to(uid)
344
- items = [{**v,'id':k} for k,v in (ref.get() or {}).items()]
345
- df = pd.DataFrame(items)
346
- if df.empty:
347
- return jsonify(daily_spending=[], recent_transactions=[])
348
- df['parsed'] = pd.to_datetime(df['date'], errors='coerce').fillna(pd.Timestamp('2000-01-01'))
349
- df['date_only'] = df['parsed'].dt.date.astype(str)
350
- daily = df.groupby('date_only')['total'].sum().reset_index().rename(columns={'date_only':'date'})
351
- recent = df.sort_values('timestamp',ascending=False).head(10).drop(columns=['parsed'])
352
- return jsonify(
353
- daily_spending=daily.to_dict(orient='records'),
354
- recent_transactions=recent.to_dict(orient='records')
355
- )
356
-
357
- @app.route('/api/user/profile', methods=['GET'])
358
- def user_profile():
359
- uid = get_auth_uid()
360
- u = auth.get_user(uid)
361
- d = db.reference(f'users/{uid}').get() or {}
362
- return jsonify(
363
- uid=uid, email=u.email,
364
- daily_cash=d.get('daily_cash',0),
365
- remaining_cash=d.get('remaining_cash',0),
366
- last_reset=d.get('last_reset'),
367
- is_admin=d.get('is_admin',False)
368
- )
369
-
370
- # ========================================
371
- # ORG-LEVEL ADMIN
372
- # ========================================
373
- @app.route('/api/orgs/<org_id>/admin/overview', methods=['GET'])
374
- def org_admin_overview(org_id):
375
- uid = get_auth_uid()
376
- try: org = verify_org_manager(uid, org_id)
377
- except PermissionError as e: return jsonify(error=str(e)), 403
378
-
379
- members = []
380
- for m in org.get('members',[]):
381
- try: em = auth.get_user(m).email
382
- except: em = None
383
- role = 'owner' if m==org['owner'] else 'manager' if m in org['managers'] else 'member'
384
- members.append(dict(uid=m,email=em,role=role))
385
-
386
- txs = []
387
- all_t = db.reference('transactions').get() or {}
388
- for tid,td in all_t.items():
389
- p = db.reference(f'projects/{td.get("project_id","")}').get() or {}
390
- if p.get('org_id')==org_id:
391
- txs.append({**td,'id':tid})
392
-
393
- return jsonify(
394
- members=members,
395
- transactions=txs,
396
- analytics=dict(
397
- total_members=len(members),
398
- total_transactions=len(txs),
399
- total_spent=sum(t['total'] for t in txs)
400
- )
401
- )
402
-
403
- @app.route('/api/orgs/<org_id>/admin/users/<mid>/role', methods=['PUT'])
404
- def org_admin_set_role(org_id, mid):
405
- uid = get_auth_uid()
406
- try: verify_org_manager(uid, org_id)
407
- except PermissionError as e: return jsonify(error=str(e)), 403
408
- r = (request.get_json() or {}).get('role')
409
- if r not in ('member','manager'): return jsonify(error='invalid role'),400
410
- ref = db.reference(f'organizations/{org_id}')
411
- org = ref.get() or {}
412
- mng = set(org.get('managers',[]))
413
- if r=='manager': mng.add(mid)
414
- else: mng.discard(mid)
415
- ref.update(managers=list(mng))
416
- return jsonify(success=True)
417
-
418
- @app.route('/api/orgs/<org_id>/admin/users/<mid>', methods=['DELETE'])
419
- def org_admin_remove_user(org_id, mid):
420
- uid = get_auth_uid()
421
- try: verify_org_manager(uid, org_id)
422
- except PermissionError as e: return jsonify(error=str(e)), 403
423
- ref = db.reference(f'organizations/{org_id}')
424
- org = ref.get() or {}
425
- if mid==org.get('owner'): return jsonify(error='Cannot remove owner'),400
426
- m_set, mg_set = set(org.get('members',[])), set(org.get('managers',[]))
427
- m_set.discard(mid); mg_set.discard(mid)
428
- ref.update(members=list(m_set), managers=list(mg_set))
429
- return jsonify(success=True)
430
-
431
- @app.route('/api/orgs/<org_id>transactions/<tid>', methods=['PUT','DELETE'])
432
- def modify_transaction(tid):
433
- try:
434
- verify_org_manager(request.headers.get('Authorization',''))
435
- ref = db.reference(f'transactions/{tid}')
436
- if request.method=='PUT':
437
- ref.update(request.get_json() or {})
438
- else:
439
- ref.delete()
440
- return jsonify(success=True)
441
- except Exception as e:
442
- return jsonify(error=str(e)),500
443
-
444
- # ========================================
445
- # GLOBAL ADMIN (developer)
446
- # ========================================
447
- @app.route('/api/admin/overview', methods=['GET'])
448
- def get_admin_overview():
449
- try:
450
- verify_global_admin(request.headers.get('Authorization',''))
451
- users = db.reference('users').get() or {}
452
- ulist = [{'uid':u,'email':(auth.get_user(u).email if auth.get_user(u) else None),
453
- 'is_admin':d.get('is_admin',False)} for u,d in users.items()]
454
- txs = db.reference('transactions').get() or {}
455
- tlist = [{**v,'id':k} for k,v in txs.items()]
456
- return jsonify(
457
- users=ulist, transactions=tlist,
458
- analytics=dict(
459
- total_users=len(ulist),
460
- total_transactions=len(tlist),
461
- total_spent=sum(t['total'] for t in tlist)
462
- )
463
- )
464
- except Exception as e:
465
- return jsonify(error=str(e)), 500
466
-
467
- @app.route('/api/admin/users', methods=['POST'])
468
- def create_user_admin():
469
- try:
470
- verify_global_admin(request.headers.get('Authorization',''))
471
- d = request.get_json() or {}
472
- user = auth.create_user(email=d['email'], password=d['password'])
473
- db.reference(f'users/{user.uid}').set({
474
- 'daily_cash': d.get('daily_cash',0),
475
- 'remaining_cash': d.get('daily_cash',0),
476
- 'last_reset': datetime.now(pytz.UTC).isoformat(),
477
- 'is_admin': d.get('is_admin',False)
478
- })
479
- return jsonify(success=True, user=dict(uid=user.uid,email=user.email)),201
480
- except Exception as e:
481
- return jsonify(error=str(e)),400
482
-
483
- @app.route('/api/admin/users/<uid>/reset-password', methods=['PUT'])
484
- def admin_reset_password(uid):
485
- try:
486
- verify_global_admin(request.headers.get('Authorization',''))
487
- npw = (request.get_json() or {}).get('new_password')
488
- if not npw: return jsonify(error='new_password required'),400
489
- auth.update_user(uid, password=npw)
490
- return jsonify(success=True)
491
- except Exception as e:
492
- return jsonify(error=str(e)),400
493
-
494
- @app.route('/api/admin/users/<uid>', methods=['DELETE'])
495
- def delete_user(uid):
496
- try:
497
- verify_global_admin(request.headers.get('Authorization',''))
498
- auth.delete_user(uid)
499
- db.reference(f'users/{uid}').delete()
500
- txs = db.reference('transactions').order_by_child('uid').equal_to(uid).get() or {}
501
- for k in txs: db.reference(f'transactions/{k}').delete()
502
- return jsonify(success=True)
503
- except Exception as e:
504
- return jsonify(error=str(e)),500
505
-
506
-
507
- # ========================================
508
  if __name__ == '__main__':
509
  app.run(debug=True, host="0.0.0.0", port=7860)
 
20
  CORS(app)
21
 
22
  # ---------- Firebase initialization ----------
 
23
  Firebase_Storage = os.getenv("Firebase_Storage")
24
 
25
  try:
 
27
  if cred_json:
28
  cred = credentials.Certificate(json.loads(cred_json))
29
  firebase_admin.initialize_app(cred, {
 
30
  'storageBucket': Firebase_Storage
31
  })
32
  else:
 
38
 
39
  # ---------- Helper functions ----------
40
  def configure_gemini():
41
+ genai.configure(api_key=os.getenv("Gemini"))
42
  return genai.GenerativeModel('gemini-2.0-flash-thinking-exp')
43
 
 
 
 
 
 
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  if __name__ == '__main__':
46
  app.run(debug=True, host="0.0.0.0", port=7860)