Skip to content

Commit 9288274

Browse files
committed
various imrpovements, br version Agregar soporte para versión brasileña de Mercado Livre #7
1 parent bd8fb18 commit 9288274

File tree

4 files changed

+249
-48
lines changed

4 files changed

+249
-48
lines changed

app.py

Lines changed: 99 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,31 @@
2121
API_URL = "https://fastapiproject-1-eziw.onrender.com/blue"
2222
prices_cache = {}
2323

24+
# Domain configurations
25+
COUNTRY_CONFIG = {
26+
'ar': {
27+
'domain': 'mercadolibre.com.ar',
28+
'currency': 'ARS',
29+
'country_name': 'Argentina',
30+
'exchange_rate_api': API_URL,
31+
'needs_exchange_rate': True
32+
},
33+
'br': {
34+
'domain': 'mercadolivre.com.br',
35+
'currency': 'BRL',
36+
'country_name': 'Brasil',
37+
'exchange_rate_api': None,
38+
'needs_exchange_rate': False,
39+
'fixed_usd_rate': 5.0 # Fixed approximate exchange rate for Brazil (1 USD = ~5 BRL)
40+
}
41+
}
42+
43+
# Default exchange rates (will be updated by API calls)
44+
exchange_rates = {
45+
'ar': None,
46+
'br': None
47+
}
48+
2449

2550
@app.template_filter("format_number")
2651
def format_number(value):
@@ -30,29 +55,42 @@ def format_number(value):
3055
return value
3156

3257

33-
exchange_rate = None
34-
35-
36-
def get_exchange_rate():
58+
def get_exchange_rate(country_code='ar'):
3759
"""Fetch the current exchange rate from the API."""
38-
global exchange_rate
39-
if exchange_rate is not None:
40-
return exchange_rate
60+
global exchange_rates
61+
62+
country_config = COUNTRY_CONFIG[country_code]
63+
64+
# If we don't need an exchange rate for this country, use the fixed rate
65+
if not country_config['needs_exchange_rate']:
66+
return f"{country_config['fixed_usd_rate']} {country_config['currency']}"
67+
68+
# If we already have a cached rate, return it
69+
if exchange_rates[country_code] is not None:
70+
return exchange_rates[country_code]
71+
72+
# Otherwise fetch from API (for Argentina)
4173
try:
42-
response = requests.get(API_URL)
74+
response = requests.get(country_config['exchange_rate_api'])
4375
response.raise_for_status()
44-
exchange_rate = response.json().get("venta", None)
45-
return exchange_rate
76+
exchange_rates[country_code] = response.json().get("venta", None)
77+
return exchange_rates[country_code]
4678
except requests.exceptions.RequestException as e:
47-
app.logger.error(f"Error fetching exchange rate: {e}")
48-
return None
79+
app.logger.error(f"Error fetching exchange rate for {country_code}: {e}")
80+
# Return a fallback value if the API fails
81+
return f"1000 {country_config['currency']}"
4982

5083

5184
@app.route("/", methods=["GET", "POST"])
5285
def index():
5386
if request.method == "POST":
5487
item = request.form["item"].strip()
5588
number_of_pages = request.form["number_of_pages"].strip()
89+
country_code = request.form.get("country", "ar") # Default to Argentina if not specified
90+
91+
# Validate country code
92+
if country_code not in COUNTRY_CONFIG:
93+
return render_template("error.html", error_message="Invalid country code."), 400
5694

5795
# Validate item
5896
if not item or not re.match(r"^[a-zA-Z0-9\-\s]+$", item):
@@ -66,7 +104,7 @@ def index():
66104
except ValueError:
67105
return render_template("error.html", error_message="Number of pages must be a valid integer."), 400
68106

69-
return redirect(url_for("show_plot", item=item, number_of_pages=number_of_pages))
107+
return redirect(url_for("show_plot", item=item, number_of_pages=number_of_pages, country=country_code))
70108
return render_template("index.html")
71109

72110

@@ -85,17 +123,19 @@ def serve_assetlinks():
85123
return send_file('assetlinks.json', mimetype='application/json')
86124

87125

88-
def get_prices(item, number_of_pages):
89-
"""Fetch the prices of the given item from MercadoLibre."""
90-
cache_key = (item, number_of_pages)
126+
def get_prices(item, number_of_pages, country_code='ar'):
127+
"""Fetch the prices of the given item from MercadoLibre/MercadoLivre."""
128+
cache_key = (item, number_of_pages, country_code)
91129
if cache_key in prices_cache:
92130
return prices_cache[cache_key]
93131

94132
prices_list = []
95133
failed_pages = 0
134+
domain = COUNTRY_CONFIG[country_code]['domain']
135+
96136
for i in range(number_of_pages):
97137
start_item = i * 50 + 1
98-
url = f"https://listado.mercadolibre.com.ar/{item}_Desde_{start_item}_NoIndex_True"
138+
url = f"https://listado.{domain}/{item}_Desde_{start_item}_NoIndex_True"
99139
try:
100140
response = requests.get(url)
101141
response.raise_for_status()
@@ -116,6 +156,11 @@ def get_prices(item, number_of_pages):
116156
app.logger.info("No results found for the given search.")
117157
return None, None, failed_pages
118158

159+
# For Brazilian prices, convert from centavos to reais if needed
160+
if country_code == 'br' and any(p > 10000 for p in prices_list):
161+
# Check if the prices seem to be in centavos (very high numbers)
162+
prices_list = [p / 100 for p in prices_list]
163+
119164
prices_cache[cache_key] = (prices_list, url, failed_pages)
120165
return prices_list, url, failed_pages
121166

@@ -125,12 +170,21 @@ def format_x(value, tick_number):
125170
return f"{int(value):,}"
126171

127172

128-
def plot_prices(prices_list, item, url, failed_pages, filter_outliers=True, threshold=3):
129-
venta_dolar = get_exchange_rate()
130-
if not venta_dolar:
131-
app.logger.error("Failed to get exchange rate.")
173+
def plot_prices(prices_list, item, url, failed_pages, country_code='ar', filter_outliers=True, threshold=3):
174+
country_config = COUNTRY_CONFIG[country_code]
175+
currency = country_config['currency']
176+
country_name = country_config['country_name']
177+
178+
venta_dolar_str = get_exchange_rate(country_code)
179+
if not venta_dolar_str:
180+
app.logger.error(f"Failed to get exchange rate for {country_code}.")
132181
return None
133-
venta_dolar = float(venta_dolar.replace(" ARS", ""))
182+
183+
try:
184+
venta_dolar = float(venta_dolar_str.replace(f" {currency}", ""))
185+
except (ValueError, TypeError):
186+
app.logger.error(f"Failed to parse exchange rate: {venta_dolar_str}")
187+
venta_dolar = country_config.get('fixed_usd_rate', 1000) # Fallback
134188

135189
# Compute statistics on the full dataset
136190
std_dev = np.std(prices_list)
@@ -165,11 +219,11 @@ def plot_prices(prices_list, item, url, failed_pages, filter_outliers=True, thre
165219
else 10000
166220
)
167221

168-
plt.xlabel("Price in ARS")
222+
plt.xlabel(f"Price in {currency}")
169223
plt.ylabel("Frequency")
170224
current_date = datetime.date.today().strftime("%d/%m/%Y")
171225
plt.title(
172-
f'Histogram of {item.replace("-", " ").upper()} prices in MercadoLibre Argentina ({current_date})\n'
226+
f'Histogram of {item.replace("-", " ").upper()} prices in MercadoLi{"v" if country_code == "br" else "b"}re {country_name} ({current_date})\n'
173227
f"Number of items indexed: {len(prices_list)} ({request.args.get('number_of_pages')} pages)\n"
174228
f"URL: {url}\n"
175229
f"Failed to parse {failed_pages} pages."
@@ -180,7 +234,7 @@ def plot_stat_line(stat_value, color, label, linestyle="solid", linewidth=1):
180234
plt.text(
181235
stat_value + x_pos_offset,
182236
y_position,
183-
f"{label}: {int(stat_value):,} ARS ({int(stat_value / venta_dolar):,} USD)",
237+
f"{label}: {int(stat_value):,} {currency} ({int(stat_value / venta_dolar):,} USD)",
184238
rotation=90,
185239
color=color,
186240
)
@@ -213,6 +267,11 @@ def plot_stat_line(stat_value, color, label, linestyle="solid", linewidth=1):
213267
def show_plot():
214268
item = request.args.get("item", "").strip()
215269
number_of_pages = request.args.get("number_of_pages", "").strip()
270+
country_code = request.args.get("country", "ar") # Default to Argentina if not specified
271+
272+
# Validate country code
273+
if country_code not in COUNTRY_CONFIG:
274+
return render_template("error.html", error_message="Invalid country code."), 400
216275

217276
# Validate item
218277
if not item or not re.match(r"^[a-zA-Z0-9\-\s]+$", item):
@@ -226,7 +285,7 @@ def show_plot():
226285
except ValueError:
227286
return render_template("error.html", error_message="Number of pages must be a valid integer."), 400
228287

229-
prices_list, url, failed_pages = get_prices(item, number_of_pages)
288+
prices_list, url, failed_pages = get_prices(item, number_of_pages, country_code)
230289
if prices_list is None or url is None:
231290
error_message = "Failed to fetch prices. Please try searching fewer pages or check the item name."
232291
return render_template("error.html", error_message=error_message), 500
@@ -238,10 +297,19 @@ def show_plot():
238297
std_dev = float(np.std(prices_list))
239298
percentile_25 = float(np.percentile(prices_list, 25))
240299
current_date = datetime.date.today().strftime("%d/%m/%Y")
241-
exchange_rate = float(get_exchange_rate().replace(" ARS", ""))
300+
301+
country_config = COUNTRY_CONFIG[country_code]
302+
currency = country_config['currency']
303+
304+
# Get exchange rate - use fixed rate for Brazil
305+
if country_config['needs_exchange_rate']:
306+
exchange_rate_str = get_exchange_rate(country_code)
307+
exchange_rate = float(exchange_rate_str.replace(f" {currency}", ""))
308+
else:
309+
exchange_rate = country_config['fixed_usd_rate']
242310

243311
# Generate the plot for backward compatibility and image download
244-
plot_base64 = plot_prices(prices_list, item, url, failed_pages)
312+
plot_base64 = plot_prices(prices_list, item, url, failed_pages, country_code)
245313
if plot_base64 is None:
246314
error_message = "Failed to generate plot. Please try again later."
247315
return render_template("error.html", error_message=error_message), 500
@@ -278,6 +346,9 @@ def show_plot():
278346
max_price_usd=int(max_price / exchange_rate),
279347
min_price_usd=int(min_price / exchange_rate),
280348
failed_pages=failed_pages,
349+
country_code=country_code,
350+
currency=country_config['currency'],
351+
country_name=country_config['country_name'],
281352
)
282353

283354

static/js/chart.js

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ document.addEventListener('DOMContentLoaded', function() {
1515
const numberOfPages = chartContainer.dataset.pages || '1';
1616
const failedPages = chartContainer.dataset.failed || '0';
1717
const exchangeRate = parseFloat(chartContainer.dataset.exchange || '0');
18+
const currency = chartContainer.dataset.currency || 'ARS';
19+
const countryName = chartContainer.dataset.country || 'Argentina';
20+
const countryCode = chartContainer.dataset.countryCode || 'ar';
1821

1922
// Calculate statistics (already available in the HTML, but recalculated for completeness)
2023
const medianPrice = parseFloat(chartContainer.dataset.median || '0');
@@ -27,14 +30,27 @@ document.addEventListener('DOMContentLoaded', function() {
2730
function formatNumber(num) {
2831
return new Intl.NumberFormat('es-AR').format(Math.round(num));
2932
}
33+
34+
// Function to format numbers with abbreviations (K, M)
35+
function abbreviateNumber(num) {
36+
if (num >= 1000000) {
37+
return (num / 1000000).toFixed(1) + 'M';
38+
} else if (num >= 1000) {
39+
return (num / 1000).toFixed(1) + 'K';
40+
}
41+
return Math.round(num).toString();
42+
}
3043

3144
// Generate histogram data
3245
const histogramData = generateHistogramData(priceData);
46+
47+
// Determine if we're using MercadoLibre or MercadoLivre based on country
48+
const marketplaceName = countryCode === 'br' ? 'MercadoLivre' : 'MercadoLibre';
3349

3450
// Chart options
3551
const option = {
3652
title: {
37-
text: `Histogram of ${item.replace('-', ' ').toUpperCase()} prices in MercadoLibre Argentina (${currentDate})`,
53+
text: `Histogram of ${item.replace('-', ' ').toUpperCase()} prices in ${marketplaceName} ${countryName} (${currentDate})`,
3854
subtext: `Number of items indexed: ${priceData.length} (${numberOfPages} pages)\nURL: ${url}\nFailed to parse ${failedPages} pages.`,
3955
left: 'center'
4056
},
@@ -46,14 +62,19 @@ document.addEventListener('DOMContentLoaded', function() {
4662
formatter: function(params) {
4763
const range = params[0].name;
4864
const count = params[0].value;
49-
const ars = range.split(' - ')[0].replace('ARS ', '');
65+
const localCurrency = range.split(' - ')[0].replace(`${currency} `, '');
5066

51-
// Calculate USD value
52-
const usdValue = Math.round(parseInt(ars.replace(/,/g, '')) / exchangeRate);
67+
// Only show USD conversion if exchange rate is valid and country is Argentina
68+
let usdPart = '';
69+
if (countryCode === 'ar' && exchangeRate > 0) {
70+
const usdValue = Math.round(parseInt(localCurrency.replace(/,/g, '')) / exchangeRate);
71+
if (usdValue > 12) {
72+
usdPart = `<br><strong>USD:</strong> ~${formatNumber(usdValue)} USD`;
73+
}
74+
}
5375

5476
return `<strong>Price Range:</strong> ${range}<br>
55-
<strong>Count:</strong> ${count} items<br>
56-
<strong>USD:</strong> ~${formatNumber(usdValue)} USD`;
77+
<strong>Count:</strong> ${count} items${usdPart}`;
5778
}
5879
},
5980
toolbox: {
@@ -72,11 +93,20 @@ document.addEventListener('DOMContentLoaded', function() {
7293
xAxis: {
7394
type: 'category',
7495
data: histogramData.bins,
75-
name: 'Price in ARS',
96+
name: `Price in ${currency}`,
7697
nameLocation: 'middle',
7798
nameGap: 30,
7899
axisLabel: {
79-
rotate: 45
100+
rotate: 45,
101+
formatter: function(value) {
102+
// Extract the price value from the range string
103+
const priceMatch = value.match(/(\d[\d.,]*)/);
104+
if (priceMatch && priceMatch[1]) {
105+
const priceValue = parseFloat(priceMatch[1].replace(/[,.]/g, ''));
106+
return `${currency} ${abbreviateNumber(priceValue)}`;
107+
}
108+
return value;
109+
}
80110
}
81111
},
82112
yAxis: {
@@ -137,11 +167,14 @@ function generateHistogramData(prices) {
137167
const bins = [];
138168
const counts = [];
139169

170+
// Get currency from data attributes
171+
const currency = document.getElementById('chart-container').dataset.currency || 'ARS';
172+
140173
// Fill bins and initialize counts
141174
for (let i = 0; i < numberOfBins; i++) {
142175
const binStart = min + i * binSize;
143176
const binEnd = min + (i + 1) * binSize;
144-
bins.push(`ARS ${formatNumber(binStart)} - ${formatNumber(binEnd)}`);
177+
bins.push(`${currency} ${formatNumber(binStart)} - ${formatNumber(binEnd)}`);
145178
counts.push(0);
146179
}
147180

@@ -156,14 +189,16 @@ function generateHistogramData(prices) {
156189

157190
// Function to create a mark line for statistics
158191
function createMarkLine(name, value, color, lineType = 'solid') {
192+
const currency = document.getElementById('chart-container').dataset.currency || 'ARS';
193+
159194
return {
160195
name,
161196
type: 'line',
162197
markLine: {
163198
silent: true,
164199
symbol: 'none',
165200
label: {
166-
formatter: `${name}: {c} ARS`,
201+
formatter: `${name}: {c} ${currency}`,
167202
position: 'insideEndTop',
168203
fontSize: 12,
169204
color: color,

0 commit comments

Comments
 (0)