Starchik1 commited on
Commit
d419eef
·
verified ·
1 Parent(s): 57d22ba

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +14 -622
main.py CHANGED
@@ -1,638 +1,30 @@
1
  from flask import Flask, request, Response
2
  import requests
3
- import logging
4
- from urllib.parse import urljoin
5
- from urllib.parse import quote
6
- from bs4 import BeautifulSoup
7
- import re
8
-
9
- # Configure logging
10
- logging.basicConfig(level=logging.INFO)
11
- logger = logging.getLogger(__name__)
12
 
13
  app = Flask(__name__)
 
14
 
15
- # Target URL base
16
- TARGET_BASE_URL = "https://superetka.com"
17
-
18
- @app.route('/proxy_image')
19
- def proxy_image():
20
- from urllib.parse import unquote
21
- image_url = unquote(request.args.get('url'))
22
- if not image_url:
23
- return 'No URL provided', 400
24
-
25
- try:
26
- # Get headers from the incoming request
27
- headers = {key: value for key, value in request.headers if key.lower() != 'host'}
28
- headers['Referer'] = TARGET_BASE_URL
29
- headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
30
-
31
- # Forward the request to get the image
32
- resp = requests.get(
33
- image_url,
34
- headers=headers,
35
- cookies=request.cookies,
36
- timeout=30,
37
- allow_redirects=True
38
- )
39
-
40
- # Check response status
41
- if resp.status_code != 200:
42
- logger.error(f"Error response from target: {resp.status_code}")
43
- return f"Error: {resp.status_code}", resp.status_code
44
-
45
- return Response(resp.content, mimetype=resp.headers.get('Content-Type', 'image/jpeg'))
46
- except Exception as e:
47
- logger.error(f"Error proxying image: {str(e)}")
48
- return str(e), 500
49
-
50
- @app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
51
- @app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
52
  def proxy(path):
53
- # Construct target URL with path and query parameters
54
- if path and path.strip():
55
- # If path is provided, use it
56
- url = f"{TARGET_BASE_URL}/{path}"
57
- else:
58
- # Default to wap.php if no path is provided
59
- url = f"{TARGET_BASE_URL}/etka/wap.php"
60
-
61
- # If there are query parameters, append them to the URL
62
- if request.query_string:
63
- url = f"{url}?{request.query_string.decode('utf-8')}"
64
-
65
- # Log the constructed URL
66
- logger.info(f"Constructed URL: {url}")
67
-
68
- # Log the request
69
- logger.info(f"Received request: {request.method} {request.url}")
70
- logger.info(f"Forwarding to: {url}")
71
-
72
- # Get headers from the incoming request
73
- headers = {key: value for key, value in request.headers if key.lower() != 'host'}
74
-
75
  try:
76
- # Forward the request to the target server
77
- # Don't pass params separately as they're already in the URL
78
  resp = requests.request(
79
  method=request.method,
80
- url=url,
81
  headers=headers,
82
  data=request.get_data(),
83
  cookies=request.cookies,
84
  allow_redirects=False,
85
- timeout=30
86
  )
87
-
88
- # Log the response
89
- logger.info(f"Received response from target: {resp.status_code}")
90
-
91
- # Check if response is HTML and filter content if needed
92
- content_type = resp.headers.get('Content-Type', '')
93
- if 'text/html' in content_type:
94
- # Parse HTML content
95
- html_content = resp.content.decode('utf-8', errors='ignore')
96
- soup = BeautifulSoup(html_content, 'html.parser')
97
-
98
- # Filter out "Полная версия ETKA"
99
- for element in soup.find_all(string=re.compile('Полная версия ETKA')):
100
- # Replace the text with empty string
101
- element.replace_with('')
102
-
103
- # Filter out README content
104
- for element in soup.find_all(string=re.compile('README', re.IGNORECASE)):
105
- element.replace_with('')
106
-
107
- # Redirect part number links to Google search
108
- # Look for links that contain part numbers (typically in the second column of the table)
109
- part_number_links = soup.select('td:nth-child(2) a')
110
- for link in part_number_links:
111
- # Get the part number from the link text
112
- part_number = link.text.strip()
113
- # Check if it matches a part number pattern (alphanumeric with possible spaces)
114
- if re.match(r'^[A-Z0-9 ]+$', part_number):
115
- # Create a Google search URL for this part number with avto.pro
116
- google_search_url = f"https://www.google.com/search?q={part_number} avto.pro"
117
- # Update the link's href attribute and add target="_blank" to open in new tab
118
- link['href'] = google_search_url
119
- link['target'] = '_blank'
120
-
121
- # Update image sources to use our proxy
122
- for img in soup.find_all('img'):
123
- img_src = img['src']
124
- if not img_src.startswith('http'):
125
- img_src = urljoin(TARGET_BASE_URL, img_src)
126
- proxy_url = f"/proxy_image?url={quote(img_src)}"
127
- img['src'] = proxy_url
128
- img['style'] = 'max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 10px 0;'
129
-
130
- # Add "Open Original Image" button next to each image
131
- button = soup.new_tag('a')
132
- button['href'] = proxy_url
133
- button['target'] = '_blank'
134
- button['class'] = 'open-image-btn'
135
- button.string = 'Открыть оригинал изображения'
136
- button['style'] = 'display: inline-block; margin: 10px; padding: 8px 15px; background-color: #4a90e2; border: none; border-radius: 4px; text-decoration: none; color: white; font-weight: 500; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'
137
- img.insert_after(button)
138
-
139
- # Add modal container for fullscreen image view
140
- modal_div = soup.new_tag('div')
141
- modal_div['id'] = 'imageModal'
142
- modal_div['class'] = 'modal'
143
- modal_div['style'] = 'display: none;'
144
-
145
- modal_img = soup.new_tag('img')
146
- modal_img['id'] = 'modalImage'
147
- modal_img['src'] = ''
148
- modal_img['alt'] = 'Fullscreen Image'
149
-
150
- close_btn = soup.new_tag('span')
151
- close_btn['class'] = 'close'
152
- close_btn.string = '×'
153
-
154
- modal_div.append(close_btn)
155
- modal_div.append(modal_img)
156
-
157
- if soup.body:
158
- soup.body.append(modal_div)
159
- else:
160
- body_tag = soup.new_tag('body')
161
- body_tag.append(modal_div)
162
- if soup.html:
163
- soup.html.append(body_tag)
164
- else:
165
- html_tag = soup.new_tag('html')
166
- html_tag.append(body_tag)
167
- soup.append(html_tag)
168
-
169
- # Add JavaScript for modal functionality
170
- script_tag = soup.new_tag('script')
171
- script_tag.string = '''
172
- document.addEventListener('DOMContentLoaded', function() {
173
- var modal = document.getElementById('imageModal');
174
- var modalImg = document.getElementById('modalImage');
175
- var closeBtn = document.getElementsByClassName('close')[0];
176
- var scale = 1;
177
- var isDragging = false;
178
- var startX, startY, translateX = 0, translateY = 0;
179
-
180
- // Add zoom controls
181
- var zoomControls = document.createElement('div');
182
- zoomControls.className = 'zoom-controls';
183
- zoomControls.innerHTML = `
184
- <button class="zoom-btn" onclick="changeZoom(0.1)">+</button>
185
- <button class="zoom-btn" onclick="changeZoom(-0.1)">-</button>
186
- <button class="zoom-btn" onclick="resetZoom()">Reset</button>
187
- `;
188
- modal.appendChild(zoomControls);
189
-
190
- // Zoom functions
191
- window.changeZoom = function(delta) {
192
- scale = Math.min(Math.max(scale + delta, 0.5), 3);
193
- applyTransform();
194
- };
195
-
196
- window.resetZoom = function() {
197
- scale = 1;
198
- translateX = 0;
199
- translateY = 0;
200
- applyTransform();
201
- };
202
-
203
- function applyTransform() {
204
- modalImg.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
205
- }
206
-
207
- // Mouse wheel zoom
208
- modal.addEventListener('wheel', function(e) {
209
- e.preventDefault();
210
- const delta = e.deltaY * -0.001;
211
- scale = Math.min(Math.max(scale + delta, 0.5), 3);
212
- applyTransform();
213
- });
214
-
215
- // Touch gestures for mobile
216
- let initialDistance = 0;
217
- modal.addEventListener('touchstart', function(e) {
218
- if (e.touches.length === 2) {
219
- initialDistance = Math.hypot(
220
- e.touches[0].pageX - e.touches[1].pageX,
221
- e.touches[0].pageY - e.touches[1].pageY
222
- );
223
- }
224
- });
225
-
226
- modal.addEventListener('touchmove', function(e) {
227
- if (e.touches.length === 2) {
228
- e.preventDefault();
229
- const currentDistance = Math.hypot(
230
- e.touches[0].pageX - e.touches[1].pageX,
231
- e.touches[0].pageY - e.touches[1].pageY
232
- );
233
- const delta = (currentDistance - initialDistance) * 0.01;
234
- scale = Math.min(Math.max(scale + delta, 0.5), 3);
235
- initialDistance = currentDistance;
236
- applyTransform();
237
- }
238
- });
239
-
240
- // Add click event to all images
241
- document.querySelectorAll('img:not(#modalImage)').forEach(function(img) {
242
- img.style.cursor = 'pointer';
243
- img.addEventListener('click', function() {
244
- modal.style.display = 'flex';
245
- modalImg.src = this.src;
246
- document.body.style.overflow = 'hidden';
247
- scale = 1;
248
- translateX = 0;
249
- translateY = 0;
250
- applyTransform();
251
- setTimeout(function() {
252
- modal.classList.add('show');
253
- }, 10);
254
- });
255
- });
256
-
257
- // Close modal on click
258
- function closeModal() {
259
- modal.classList.remove('show');
260
- setTimeout(function() {
261
- modal.style.display = 'none';
262
- document.body.style.overflow = 'auto';
263
- }, 300);
264
- }
265
-
266
- closeBtn.onclick = closeModal;
267
- modal.onclick = function(e) {
268
- if (e.target === modal) closeModal();
269
- };
270
-
271
- // Close on escape key
272
- document.addEventListener('keydown', function(e) {
273
- if (e.key === 'Escape') closeModal();
274
- });
275
-
276
- // Image dragging
277
- modalImg.addEventListener('mousedown', function(e) {
278
- isDragging = true;
279
- startX = e.clientX - translateX;
280
- startY = e.clientY - translateY;
281
- modalImg.style.cursor = 'grabbing';
282
- });
283
-
284
- document.addEventListener('mousemove', function(e) {
285
- if (isDragging) {
286
- translateX = e.clientX - startX;
287
- translateY = e.clientY - startY;
288
- applyTransform();
289
- }
290
- });
291
-
292
- document.addEventListener('mouseup', function() {
293
- isDragging = false;
294
- modalImg.style.cursor = 'grab';
295
- });
296
- });
297
- '''
298
-
299
- if soup.body:
300
- soup.body.append(script_tag)
301
-
302
- # Add CSS styles to improve design
303
- style_tag = soup.new_tag('style')
304
- style_tag.string = '''
305
- :root {
306
- --primary-gradient: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
307
- --hover-gradient: linear-gradient(135deg, #2980b9 0%, #2471a3 100%);
308
- --bg-gradient: linear-gradient(135deg, #f5f7fa 0%, #e4e9f2 100%);
309
- --card-shadow: 0 8px 20px rgba(0,0,0,0.08);
310
- --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
311
- }
312
- body {
313
- font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
314
- line-height: 1.7;
315
- padding: 30px;
316
- max-width: 1400px;
317
- margin: 0 auto;
318
- background: var(--bg-gradient);
319
- color: #2c3e50;
320
- min-height: 100vh;
321
- }
322
- .breadcrumb {
323
- display: flex;
324
- flex-wrap: wrap;
325
- gap: 15px;
326
- align-items: center;
327
- padding: 20px;
328
- background: rgba(255, 255, 255, 0.98);
329
- border-radius: 16px;
330
- box-shadow: var(--card-shadow);
331
- margin-bottom: 30px;
332
- backdrop-filter: blur(10px);
333
- border: 1px solid rgba(255, 255, 255, 0.8);
334
- }
335
- .breadcrumb a {
336
- text-decoration: none;
337
- color: #3498db;
338
- padding: 8px 16px;
339
- border-radius: 8px;
340
- transition: var(--transition);
341
- font-weight: 500;
342
- background: rgba(52, 152, 219, 0.1);
343
- }
344
- .breadcrumb a:hover {
345
- background: var(--primary-gradient);
346
- transform: translateY(-2px);
347
- color: white;
348
- }
349
- .breadcrumb > span:after {
350
- content: '›';
351
- margin-left: 15px;
352
- color: #95a5a6;
353
- font-size: 1.4em;
354
- font-weight: 300;
355
- }
356
- ul {
357
- list-style: none;
358
- padding: 0;
359
- display: grid;
360
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
361
- gap: 20px;
362
- margin: 30px 0;
363
- }
364
- li {
365
- background: rgba(255, 255, 255, 0.98);
366
- border-radius: 16px;
367
- overflow: hidden;
368
- transition: var(--transition);
369
- box-shadow: var(--card-shadow);
370
- border: 1px solid rgba(255, 255, 255, 0.8);
371
- }
372
- li:hover {
373
- transform: translateY(-5px);
374
- box-shadow: 0 12px 25px rgba(0,0,0,0.1);
375
- }
376
- li a {
377
- display: block;
378
- padding: 20px;
379
- text-decoration: none;
380
- color: #2c3e50;
381
- font-weight: 500;
382
- transition: var(--transition);
383
- background: rgba(255, 255, 255, 0.98);
384
- }
385
- li a:hover {
386
- background: var(--primary-gradient);
387
- color: white;
388
- }
389
- table {
390
- width: 100%;
391
- border-collapse: separate;
392
- border-spacing: 0;
393
- background: rgba(255, 255, 255, 0.98);
394
- border-radius: 16px;
395
- overflow: hidden;
396
- box-shadow: var(--card-shadow);
397
- margin: 30px 0;
398
- backdrop-filter: blur(10px);
399
- border: 1px solid rgba(255, 255, 255, 0.8);
400
- }
401
- td {
402
- padding: 20px;
403
- border: 1px solid rgba(236, 240, 241, 0.8);
404
- transition: var(--transition);
405
- }
406
- tr:nth-child(even) {
407
- background: rgba(245, 247, 250, 0.5);
408
- }
409
- tr:hover {
410
- background: var(--bg-gradient);
411
- transform: scale(1.002);
412
- }
413
- img {
414
- border-radius: 16px;
415
- box-shadow: var(--card-shadow);
416
- transition: var(--transition);
417
- max-width: 100%;
418
- height: auto;
419
- display: block;
420
- margin: 10px 0;
421
- }
422
- img:hover {
423
- transform: scale(1.03);
424
- box-shadow: 0 12px 25px rgba(0,0,0,0.15);
425
- }
426
- .open-image-btn {
427
- display: inline-block;
428
- margin: 15px 0;
429
- padding: 12px 24px;
430
- background: var(--primary-gradient);
431
- color: white !important;
432
- border-radius: 12px;
433
- text-decoration: none;
434
- transition: var(--transition);
435
- font-weight: 500;
436
- box-shadow: var(--card-shadow);
437
- border: 1px solid rgba(255, 255, 255, 0.2);
438
- }
439
- .open-image-btn:hover {
440
- background: var(--hover-gradient);
441
- transform: translateY(-3px);
442
- box-shadow: 0 12px 25px rgba(0,0,0,0.15);
443
- }
444
- hr {
445
- border: none;
446
- height: 1px;
447
- background: linear-gradient(to right, transparent, rgba(44, 62, 80, 0.2), transparent);
448
- margin: 30px 0;
449
- }
450
- .modal {
451
- display: none;
452
- position: fixed;
453
- top: 0;
454
- left: 0;
455
- width: 100%;
456
- height: 100%;
457
- background: rgba(0, 0, 0, 0.9);
458
- z-index: 1000;
459
- justify-content: center;
460
- align-items: center;
461
- opacity: 0;
462
- transition: opacity 0.3s ease;
463
- }
464
-
465
- .modal.show {
466
- opacity: 1;
467
- }
468
-
469
- .modal img {
470
- max-width: 90%;
471
- max-height: 90vh;
472
- margin: auto;
473
- display: block;
474
- box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
475
- transform: scale(0.9);
476
- transition: transform 0.3s ease;
477
- cursor: grab;
478
- transform-origin: center center;
479
- }
480
-
481
- .modal.show img {
482
- transform: scale(1);
483
- }
484
-
485
- .zoom-controls {
486
- position: fixed;
487
- bottom: 20px;
488
- left: 50%;
489
- transform: translateX(-50%);
490
- display: flex;
491
- gap: 10px;
492
- z-index: 1001;
493
- }
494
-
495
- .zoom-btn {
496
- background: rgba(255, 255, 255, 0.2);
497
- border: 1px solid rgba(255, 255, 255, 0.4);
498
- color: white;
499
- padding: 8px 16px;
500
- border-radius: 4px;
501
- cursor: pointer;
502
- font-size: 16px;
503
- transition: all 0.2s ease;
504
- }
505
-
506
- .zoom-btn:hover {
507
- background: rgba(255, 255, 255, 0.3);
508
- }
509
-
510
- .close {
511
- position: absolute;
512
- top: 15px;
513
- right: 25px;
514
- color: #f1f1f1;
515
- font-size: 40px;
516
- font-weight: bold;
517
- cursor: pointer;
518
- z-index: 1001;
519
- transition: color 0.3s ease;
520
- }
521
-
522
- .close:hover {
523
- color: #3498db;
524
- }
525
-
526
- @media (max-width: 768px) {
527
- body { padding: 15px; }
528
- .breadcrumb {
529
- flex-direction: column;
530
- align-items: flex-start;
531
- padding: 15px;
532
- }
533
- ul {
534
- grid-template-columns: 1fr;
535
- gap: 15px;
536
- }
537
- table { font-size: 14px; }
538
- td { padding: 12px; }
539
- .open-image-btn {
540
- width: 100%;
541
- text-align: center;
542
- }
543
- .modal img {
544
- max-width: 95%;
545
- max-height: 95vh;
546
- }
547
- .close {
548
- top: 10px;
549
- right: 15px;
550
- }
551
- }
552
- '''
553
- # Check if head exists, if not create it
554
- if not soup.head:
555
- head_tag = soup.new_tag('head')
556
- if soup.html:
557
- soup.html.insert(0, head_tag)
558
- else:
559
- html_tag = soup.new_tag('html')
560
- html_tag.append(head_tag)
561
- soup.append(html_tag)
562
- soup.head.append(style_tag)
563
-
564
- # Convert navigation links to breadcrumb
565
- nav_links = soup.find_all('a', href=True)
566
- breadcrumb_div = soup.new_tag('div')
567
- breadcrumb_div['class'] = 'breadcrumb'
568
-
569
- # Find the main navigation container
570
- nav_container = None
571
- for link in nav_links:
572
- if link.parent.name == 'font' and link.parent.parent.name == 'td':
573
- nav_container = link.parent.parent
574
- break
575
-
576
- if nav_container:
577
- nav_links = nav_container.find_all('a', href=True)
578
- current_span = None
579
- for link in nav_links:
580
- # Skip empty links or those without text
581
- if not link.string or not link.string.strip():
582
- continue
583
-
584
- # Skip navigation arrows and special characters
585
- if any(char in link.string for char in ['>', '<', '→', '←']):
586
- continue
587
-
588
- # Create new span for each link
589
- current_span = soup.new_tag('span')
590
- breadcrumb_div.append(current_span)
591
-
592
- # Clone the link to avoid modifying original
593
- new_link = soup.new_tag('a', href=link['href'])
594
- new_link.string = link.string.strip()
595
- current_span.append(new_link)
596
-
597
- # Replace old navigation with new breadcrumb if we found valid navigation
598
- if nav_container:
599
- nav_container.replace_with(breadcrumb_div)
600
-
601
- # Create a Flask response object with filtered content
602
- response = Response(
603
- soup.encode(),
604
- status=resp.status_code,
605
- content_type='text/html; charset=utf-8'
606
- )
607
- else:
608
- # Create a Flask response object with original content
609
- response = Response(
610
- resp.content,
611
- status=resp.status_code
612
- )
613
-
614
- # Copy headers from the target response
615
- for key, value in resp.headers.items():
616
- if key.lower() not in ('transfer-encoding', 'content-encoding', 'content-length'):
617
- response.headers[key] = value
618
-
619
- # Copy cookies from target response
620
- for cookie in resp.cookies:
621
- response.set_cookie(
622
- key=cookie.name,
623
- value=cookie.value,
624
- # domain=cookie.domain,
625
- path=cookie.path,
626
- expires=cookie.expires,
627
- secure=cookie.secure,
628
- httponly=cookie.httponly
629
- )
630
-
631
- return response
632
-
633
- except requests.RequestException as e:
634
- logger.error(f"Error forwarding request: {str(e)}")
635
- return Response(f"Error forwarding request: {str(e)}", status=500)
636
 
637
  if __name__ == '__main__':
638
- app.run(host='0.0.0.0', port=5000, debug=True)
 
1
  from flask import Flask, request, Response
2
  import requests
 
 
 
 
 
 
 
 
 
3
 
4
  app = Flask(__name__)
5
+ TARGET_URL = 'https://www.ipay.ua/ru/request-money'
6
 
7
+ @app.route('/', defaults={'path': ''})
8
+ @app.route('/<path:path>', methods=['GET', 'POST'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  def proxy(path):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  try:
11
+ headers = {key: value for (key, value) in request.headers if key != 'Host'}
12
+ headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
13
  resp = requests.request(
14
  method=request.method,
15
+ url=f'{TARGET_URL}/{path}',
16
  headers=headers,
17
  data=request.get_data(),
18
  cookies=request.cookies,
19
  allow_redirects=False,
20
+ verify=False
21
  )
22
+ excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
23
+ headers = [(name, value) for (name, value) in resp.raw.headers.items()
24
+ if name.lower() not in excluded_headers]
25
+ return Response(resp.content, resp.status_code, headers)
26
+ except Exception as e:
27
+ return str(e), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  if __name__ == '__main__':
30
+ app.run(port=5000, debug=True)