feat: добавление нового API для получения фотографий по VIN - реализована проверка доступа и наличие фотографий - улучшен UX с помощью более информативных ответов - обновлены шаблоны для улучшения дизайна и адаптивности

This commit is contained in:
Vlad 2025-04-20 01:50:25 +03:00
parent a62b1b62bd
commit 9673a839c9
6 changed files with 412 additions and 156 deletions

View File

@ -0,0 +1,98 @@
---
description:
globs:
alwaysApply: true
---
You are an expert in Python, Flask, and scalable API development.
Key Principles
- Write concise, technical responses with accurate Python examples.
- Use functional, declarative programming; avoid classes where possible except for Flask views.
- Prefer iteration and modularization over code duplication.
- Use descriptive variable names with auxiliary verbs (e.g., is_active, has_permission).
- Use lowercase with underscores for directories and files (e.g., blueprints/user_routes.py).
- Favor named exports for routes and utility functions.
- Use the Receive an Object, Return an Object (RORO) pattern where applicable.
Python/Flask
- Use def for function definitions.
- Use type hints for all function signatures where possible.
- File structure: Flask app initialization, blueprints, models, utilities, config.
- Avoid unnecessary curly braces in conditional statements.
- For single-line statements in conditionals, omit curly braces.
- Use concise, one-line syntax for simple conditional statements (e.g., if condition: do_something()).
Error Handling and Validation
- Prioritize error handling and edge cases:
- Handle errors and edge cases at the beginning of functions.
- Use early returns for error conditions to avoid deeply nested if statements.
- Place the happy path last in the function for improved readability.
- Avoid unnecessary else statements; use the if-return pattern instead.
- Use guard clauses to handle preconditions and invalid states early.
- Implement proper error logging and user-friendly error messages.
- Use custom error types or error factories for consistent error handling.
Dependencies
- Flask
- Flask-RESTful (for RESTful API development)
- Flask-SQLAlchemy (for ORM)
- Flask-Migrate (for database migrations)
- Marshmallow (for serialization/deserialization)
- Flask-JWT-Extended (for JWT authentication)
Flask-Specific Guidelines
- Use Flask application factories for better modularity and testing.
- Organize routes using Flask Blueprints for better code organization.
- Use Flask-RESTful for building RESTful APIs with class-based views.
- Implement custom error handlers for different types of exceptions.
- Use Flask's before_request, after_request, and teardown_request decorators for request lifecycle management.
- Utilize Flask extensions for common functionalities (e.g., Flask-SQLAlchemy, Flask-Migrate).
- Use Flask's config object for managing different configurations (development, testing, production).
- Implement proper logging using Flask's app.logger.
- Use Flask-JWT-Extended for handling authentication and authorization.
Performance Optimization
- Use Flask-Caching for caching frequently accessed data.
- Implement database query optimization techniques (e.g., eager loading, indexing).
- Use connection pooling for database connections.
- Implement proper database session management.
- Use background tasks for time-consuming operations (e.g., Celery with Flask).
Key Conventions
1. Use Flask's application context and request context appropriately.
2. Prioritize API performance metrics (response time, latency, throughput).
3. Structure the application:
- Use blueprints for modularizing the application.
- Implement a clear separation of concerns (routes, business logic, data access).
- Use environment variables for configuration management.
Database Interaction
- Use Flask-SQLAlchemy for ORM operations.
- Implement database migrations using Flask-Migrate.
- Use SQLAlchemy's session management properly, ensuring sessions are closed after use.
Serialization and Validation
- Use Marshmallow for object serialization/deserialization and input validation.
- Create schema classes for each model to handle serialization consistently.
Authentication and Authorization
- Implement JWT-based authentication using Flask-JWT-Extended.
- Use decorators for protecting routes that require authentication.
Testing
- Write unit tests using pytest.
- Use Flask's test client for integration testing.
- Implement test fixtures for database and application setup.
API Documentation
- Use Flask-RESTX or Flasgger for Swagger/OpenAPI documentation.
- Ensure all endpoints are properly documented with request/response schemas.
Deployment
- Use Gunicorn or uWSGI as WSGI HTTP Server.
- Implement proper logging and monitoring in production.
- Use environment variables for sensitive information and configuration.
Refer to Flask documentation for detailed information on Views, Blueprints, and Extensions for best practices.

126
app.py
View File

@ -11,6 +11,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
import io
import json
from expiring_dict import ExpiringDict
import uuid
@ -268,28 +269,17 @@ def search():
ua = request.headers.get('User-Agent')
app.logger.info(f'AgeNt: {ua}')
gmedia = False
if ua == 'Mediapartners-Google':
app.logger.info(f'Find MediaPartner Aget {ua}')
if user_ip in gips:
gmedia = True
else:
gmedia = False
try:
f = open('ips.txt','a')
f.write(f'{user_ip}\n')
finally:
f.close()
g_respone = request.form.get('g-recaptcha-response')
g_respone = request.args.get('g-recaptcha-response')
capcha_check = requests.post(url=f'{capcha_site_url}?secret={capcha_site_sec}&response={g_respone}').json()
if capcha_check['success'] == False or capcha_check['score'] <0.5:
app.logger.info(f'Google reuest: {capcha_site_url}?secret={capcha_site_sec}&response={g_respone}')
app.logger.info(f'Bad google answer: {capcha_check}')
if gmedia ==False and app_debug==True:
if app_debug==True:
req_data = save_request(request)
app.logger.info(json.dumps(req_data, indent=4, default=str))
return 'bad req', 401
return 'google recaptcha req low score', 401
if len(vin) != 17:
return 'bad vin!', 500
@ -593,7 +583,7 @@ def api_search_v2():
return response
## ищем в истории
cur.execute('''select s.odo,s.odos,s.title,s.dem1,s.dem2,s.year,s.month, json_value(i.jdata, '$.Runs_Drive') RD_Status, json_value(i.jdata, '$.Locate') Sale_Location, json_value(i.jdata, '$.RepCost') as Repear_cost,
(select count(1) from salvage_images si where si.vin = s.vin) image_count from salvagedb.salvagedb s left join addinfo i on s.num = i.numid where vin =:p1 and svin = :p2''',
(select count(1) from salvage_images si where si.vin = s.vin and fn = 1) image_count from salvagedb.salvagedb s left join addinfo i on s.num = i.numid where vin =:p1 and svin = :p2''',
{'p1': vin.upper(), 'p2': vin.upper()[:10]})
res = cur.fetchall()
@ -611,7 +601,7 @@ def api_search_v2():
'RD_Status':it[7],
'Sale_Location':it[8],
'Repear_Cost':it[9],
'Image_Count':it[10]
'Photo_Count':it[10]
})
ret = {
'status': 'found',
@ -650,9 +640,107 @@ def api_search_v2():
app.logger.error(traceback.format_exc())
return 'bad request!', 500
@app.route("/api/v2/reqimage")
@app.route("/api/v2/reqphoto")
def api_reqimage():
pass
try:
access_code = request.args.get('access_code', None)
vin = request.args.get('vin', None)
user_ip = get_ip(request) ## определение ip клиента
# базовые проверки входящих аргументов
if len(vin) != 17 or vin is None:
ret = {'status': 'incorrect vin'}
response = app.response_class(
response=json.dumps(ret),
status=200,
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
return response
if len(access_code) > 16 or access_code is None:
ret = {'status': 'incorrect access_code'}
response = app.response_class(
response=json.dumps(ret),
status=200,
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
return response
conn = pool.acquire()
cur = conn.cursor()
## проверяем access_code
cur.execute(
'select t.summ, t.found_price, t.notfound_price, t.decode_price from restfact t where access_code = :p1',
{'p1': str(access_code)})
res = cur.fetchone()
summ = res[0]
found_price = res[1]
notfound_price = res[2]
decode_price = res[3]
if summ <= 0:
ret = {'status': 'You have insufficient balance.'}
response = app.response_class(
response=json.dumps(ret),
status=200,
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
return response
cur.execute('select count(*) from salvagedb.salvage_images where vin = :p1 and fnd = 1' , {'p1':vin.upper()})
res = cur.fetchone()
img_count = int(res[0])
if img_count<1:
ret = {'status': 'Photos not found for this vin.'}
response = app.response_class(
response=json.dumps(ret),
status=200,
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
return response
else:
#req_id = uuid.uuid4().hex
#cur.execute('select rownum,ipath from salvagedb.salvage_images where vin = :p1 and fnd = 1',{'p1':vin.upper()})
cur.execute("""select GenImages('{}','{}') uid from dual""".format(access_code, vin.upper()))
res = cur.fetchall()
req_id = res[0]
cur.execute("""select fake_name from salvage_images_req where req_id = '{}}'""".format(req_id))
res = cur.fetchall()
images = []
nm = 0
for it in res:
images.append({
'num':nm,
'url':'https://salimages.salvagedb/{}/{}'.format(req_id, it)
})
nm = nm + 1
ret = {'status': 'Image_found', 'Images':images}
response = app.response_class(
response=json.dumps(ret),
status=200,
mimetype='application/json'
)
except:
app.logger.error(traceback.format_exc())
return 'bad request!', 500

5
h origin main Normal file
View File

@ -0,0 +1,5 @@
1799493 (HEAD -> main) feat: адаптация шаблонов для мобильных устройств
c2d60d9 (origin/main, origin/HEAD) Развиваем API v2: +search Поправил favicon
11f6d56 Перевод search на post
97e85ec remove readme.md
12f0b91 Init

View File

@ -11,48 +11,35 @@
title, but the fact that it has been damaged for sure. Our site helps people avoid buying a damaged vehicle
in the past.
</p>
<div class="wrapper">
<!--
-->
</div>
</div>
<br><br>
<div class="container-fluid">
<div class="row-fluid">
<center>
<form id="search_bar" action="/decode" method="post">
<div style="display: inline-block;">
<h2 id="find_your_car">ENTER <a href="https://en.wikipedia.org/wiki/Vehicle_Identification_Number"
target="_blank">VIN</a> YOU CAR</h2>
<input type="text" id="vininput" name="q" class="search_box ui-autocomplete-input" autocomplete="off"
value="" role="textbox" autofocus="" required="" validate="length_between,17,17">
<button class="g-recaptcha btn btn-primary" type="submit" data-sitekey="{{capcha_site}}" id="go_button" data-callback='onSubmit' data-action='submit' style="margin-bottom: 10px;">Check It</button>
<center>
<form id="search_bar" action="/decode" method="post">
<div class="search-container">
<div class="search-title">
<h2 id="find_your_car">ENTER <a href="https://en.wikipedia.org/wiki/Vehicle_Identification_Number" target="_blank">VIN</a> YOU CAR</h2>
</div>
</form>
<div class="search-wrapper">
<input type="text" id="vininput" name="q" class="search_box ui-autocomplete-input"
autocomplete="off" value="" role="textbox" autofocus required
validate="length_between,17,17">
<button class="g-recaptcha btn btn-primary" type="submit"
data-sitekey="{{capcha_site}}" id="go_button"
data-callback='onSubmit' data-action='submit'>Check It</button>
</div>
</div>
</form>
</center>
</div>
</div>
</div>
<script>
function onSubmit(token) {
document.getElementById("search_bar").submit();
}
</script>
<br><br>
{% endblock %}
<script>
function onSubmit(token) {
document.getElementById("search_bar").submit();
}
</script>
</div>
{% endblock %}

View File

@ -10,78 +10,62 @@
odometer rollback; and gray market vehicles. We do not claim that the car got in our databank has salvage
title, but the fact that it has been damaged for sure. Our site helps people avoid buying a damaged vehicle
in the past.
</p></div>
<div class="wrapper">
<center>
<table id="one-column-emphasis">
<tbody>
<tr>
<td>Make</td>
<td>{{det[0][1]}}</td>
</tr>
<tr>
<td>Model</td>
<td>{{det[0][2]}}</td>
</tr>
<tr>
<td>Year</td>
<td>{{det[0][3]}}</td>
</tr>
<tr>
<td>Body Style</td>
<td>{{det[0][4]}}</td>
</tr>
<tr>
<td>Engine</td>
<td>{{det[0][5]}}</td>
</tr>
<tr>
<td>Cylinders</td>
<td>{{det[0][6]}}</td>
</tr>
<tr>
<td>Drive</td>
<td>{{det[0][7]}}</td>
</tr>
</tbody>
</table>
<table style="width: 986px; height: 78px; text-align: left; margin-left: auto; margin-right: auto;" border="0" cellpadding="2" cellspacing="2">
<tbody>
<tr>
<td colspan="1" rowspan="1" style="text-align: center;"><h2>Search salvage history?</h2></td>
</tr>
<tr>
<td align="undefined" valign="undefined">
<div style="text-align: center;">
<form name="search_bar" id="search_bar" action="/search" method="post">
<input id="make_model" class="search_box ui-autocomplete-input" name="q" size="55" value="{{vin}}" autocomplete="off" spellcheck="false" type="text">
<button class="g-recaptcha btn btn-primary" data-sitekey="{{capcha_site}}" id="go_button" data-callback='onSubmit' data-action='submit' style="margin-bottom: 10px;">Search</button>
</form>
</div>
</td>
</tr>
</tbody>
</table>
</center>
<br>
<center>
</p>
</div>
<div class="details-container">
<div class="car-details">
<table id="one-column-emphasis">
<tbody>
<tr>
<td>Make</td>
<td>{{det[0][1]}}</td>
</tr>
<tr>
<td>Model</td>
<td>{{det[0][2]}}</td>
</tr>
<tr>
<td>Year</td>
<td>{{det[0][3]}}</td>
</tr>
<tr>
<td>Body Style</td>
<td>{{det[0][4]}}</td>
</tr>
<tr>
<td>Engine</td>
<td>{{det[0][5]}}</td>
</tr>
<tr>
<td>Cylinders</td>
<td>{{det[0][6]}}</td>
</tr>
<tr>
<td>Drive</td>
<td>{{det[0][7]}}</td>
</tr>
</tbody>
</table>
</div>
<div class="search-container">
<h2>Search salvage history?</h2>
<form name="search_bar" id="search_bar" action="/search" method="post">
<div class="search-wrapper">
<input id="make_model" class="search_box ui-autocomplete-input" name="q"
value="{{vin}}" autocomplete="off" spellcheck="false" type="text">
<button class="g-recaptcha btn btn-primary" data-sitekey="{{capcha_site}}"
id="go_button" data-callback='onSubmit' data-action='submit'>Search</button>
</div>
</form>
</div>
</div>
<br><br>
<script>
function onSubmit(token) {
document.getElementById("search_bar").submit();
document.getElementById("search_bar").submit();
}
</script>
<br><br>
{% endblock %}
<input value="Search" class="btn btn-primary" type="submit" id="go_button">
</script>
</div>
{% endblock %}

View File

@ -278,52 +278,137 @@
}
}
.navbar {
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
min-height: 50px;
margin-bottom: 20px;
}
.navbar-inner {
padding: 0 20px;
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 50px;
}
.navbar .brand {
padding: 10px 0;
display: flex;
align-items: center;
text-decoration: none;
color: #333;
font-weight: 600;
}
.navbar .brand img {
height: 28px;
margin-right: 10px;
}
.navbar .nav {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.navbar .nav > li {
position: relative;
}
.navbar .nav > li > a {
display: block;
padding: 15px 20px;
color: #333;
text-decoration: none;
font-weight: 600;
transition: all 0.3s;
background: #fff;
}
.navbar .nav > li > a:hover {
color: #fff;
background: #007bff;
}
.navbar .nav > li.active > a {
color: #fff;
background: #007bff;
}
.navbar .btn-navbar {
display: none;
background: none;
border: none;
padding: 10px;
cursor: pointer;
}
.navbar .btn-navbar .icon-bar {
display: block;
width: 22px;
height: 2px;
background-color: #333;
margin: 4px 0;
transition: all 0.3s;
}
@media (max-width: 768px) {
.navbar-inner {
padding: 0 10px;
}
.navbar .nav-collapse {
clear: both;
padding: 0;
padding: 0 15px;
}
.navbar .nav {
float: none;
margin: 0;
position: fixed;
top: 50px;
left: 0;
right: 0;
background: #fff;
flex-direction: column;
padding: 10px 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
transform: translateY(-100%);
opacity: 0;
visibility: hidden;
transition: all 0.3s;
z-index: 1000;
}
.navbar .nav > li {
float: none;
display: block;
.navbar .nav.active {
transform: translateY(0);
opacity: 1;
visibility: visible;
}
.navbar .nav > li > a {
padding: 10px 15px;
border-bottom: 1px solid #ddd;
padding: 15px 20px;
border-bottom: 1px solid #eee;
font-size: 16px;
font-weight: 600;
}
.navbar .nav > li > a:hover {
color: #fff;
background: #007bff;
}
.navbar .nav > li.active > a {
color: #fff;
background: #007bff;
}
.navbar .btn-navbar {
display: block;
margin: 5px 0;
background: #007bff;
border-radius: 4px;
}
.navbar .nav-collapse {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.navbar .nav-collapse.in {
max-height: 500px;
}
.navbar .brand {
padding: 10px 15px;
}
.navbar .brand img {
height: 24px;
.navbar .btn-navbar .icon-bar {
background-color: #fff;
}
}
</style>
@ -351,11 +436,20 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
var btnNavbar = document.querySelector('.btn-navbar');
var navCollapse = document.querySelector('.nav-collapse');
var nav = document.querySelector('.navbar .nav');
if (btnNavbar && navCollapse) {
if (btnNavbar && nav) {
btnNavbar.addEventListener('click', function() {
navCollapse.classList.toggle('in');
this.classList.toggle('active');
nav.classList.toggle('active');
});
// Закрытие меню при клике вне его
document.addEventListener('click', function(e) {
if (!nav.contains(e.target) && !btnNavbar.contains(e.target)) {
btnNavbar.classList.remove('active');
nav.classList.remove('active');
}
});
}
});