notify-utils 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notify_utils-0.0.1/LICENSE +21 -0
- notify_utils-0.0.1/MANIFEST.in +8 -0
- notify_utils-0.0.1/PKG-INFO +133 -0
- notify_utils-0.0.1/README.md +110 -0
- notify_utils-0.0.1/notify_utils/__init__.py +90 -0
- notify_utils-0.0.1/notify_utils/discount.py +335 -0
- notify_utils-0.0.1/notify_utils/models.py +532 -0
- notify_utils-0.0.1/notify_utils/notifiers/__init__.py +13 -0
- notify_utils-0.0.1/notify_utils/notifiers/discord_notifier.py +127 -0
- notify_utils-0.0.1/notify_utils/notifiers/formatters.py +108 -0
- notify_utils-0.0.1/notify_utils/parser.py +109 -0
- notify_utils-0.0.1/notify_utils/statistics.py +362 -0
- notify_utils-0.0.1/notify_utils/validators.py +98 -0
- notify_utils-0.0.1/notify_utils.egg-info/PKG-INFO +133 -0
- notify_utils-0.0.1/notify_utils.egg-info/SOURCES.txt +18 -0
- notify_utils-0.0.1/notify_utils.egg-info/dependency_links.txt +1 -0
- notify_utils-0.0.1/notify_utils.egg-info/requires.txt +1 -0
- notify_utils-0.0.1/notify_utils.egg-info/top_level.txt +1 -0
- notify_utils-0.0.1/pyproject.toml +51 -0
- notify_utils-0.0.1/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Naruto Uzumaki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notify-utils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Biblioteca Python para parsing de preços de scraping, cálculo de descontos e análise estatística de histórico de preços.
|
|
5
|
+
Author-email: Naruto Uzumaki <naruto_uzumaki@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jefersonAlbara/notify-utils
|
|
8
|
+
Project-URL: Repository, https://github.com/jefersonAlbara/notify-utils
|
|
9
|
+
Project-URL: Issues, https://github.com/jefersonAlbara/notify-utils/issues
|
|
10
|
+
Keywords: price-tracking,discount-calculator,web-scraping,e-commerce,price-history,discount-analysis,promotion-detection,statistics
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: discord-webhook>=1.4.1
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# notify-utils
|
|
25
|
+
|
|
26
|
+
Biblioteca Python para parsing de preços de scraping, cálculo de descontos e análise estatística de histórico de preços.
|
|
27
|
+
|
|
28
|
+
## Funcionalidades
|
|
29
|
+
|
|
30
|
+
- **Parser de Preços**: Normaliza strings de preços de diferentes formatos (BR, US)
|
|
31
|
+
- **Cálculo de Descontos**: Detecta descontos reais vs anunciados usando histórico
|
|
32
|
+
- **Análise Estatística**: Média, mediana, tendências e volatilidade de preços
|
|
33
|
+
- **Validação de Preços**: Sistema inteligente para validar preços antes de adicionar ao histórico
|
|
34
|
+
- **Notificações Discord**: Envio de alertas de preço via webhook (opcional)
|
|
35
|
+
|
|
36
|
+
## Instalação
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install notify-utils
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Uso Básico
|
|
43
|
+
|
|
44
|
+
### Parsing de Preços
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from notify_utils import parse_price
|
|
48
|
+
|
|
49
|
+
preco = parse_price("R$ 1.299,90") # → 1299.90
|
|
50
|
+
preco = parse_price("$1,299.90") # → 1299.90
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Cálculo de Desconto com Histórico
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from notify_utils import Price, get_discount_info
|
|
57
|
+
from datetime import datetime, timedelta
|
|
58
|
+
|
|
59
|
+
# Histórico de preços
|
|
60
|
+
precos = [
|
|
61
|
+
Price(value=1299.90, date=datetime.now() - timedelta(days=60)),
|
|
62
|
+
Price(value=1199.90, date=datetime.now() - timedelta(days=30)),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# Calcular desconto real baseado no histórico
|
|
66
|
+
info = get_discount_info(
|
|
67
|
+
current_price=899.90,
|
|
68
|
+
price_history=precos,
|
|
69
|
+
period_days=30
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
print(f"Desconto real: {info.discount_percentage:.2f}%")
|
|
73
|
+
print(f"É desconto real? {info.is_real_discount}")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Análise de Tendência
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from notify_utils import calculate_price_trend
|
|
80
|
+
|
|
81
|
+
trend = calculate_price_trend(precos, days=30)
|
|
82
|
+
|
|
83
|
+
print(f"Direção: {trend.direction}") # 'increasing', 'decreasing', 'stable'
|
|
84
|
+
print(f"Mudança: {trend.change_percentage:.2f}%")
|
|
85
|
+
print(f"Confiança: {trend.confidence}")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Validação de Preços
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from notify_utils import PriceHistory, Price, PriceAdditionStrategy
|
|
92
|
+
|
|
93
|
+
history = PriceHistory(product_id="PROD123", prices=precos)
|
|
94
|
+
|
|
95
|
+
# Validar antes de adicionar
|
|
96
|
+
novo_preco = Price(value=899.90, date=datetime.now())
|
|
97
|
+
result = history.add_price(
|
|
98
|
+
novo_preco,
|
|
99
|
+
strategy=PriceAdditionStrategy.SMART
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if result.action.value == "added":
|
|
103
|
+
print(f"Preço adicionado: R$ {result.affected_price.value:.2f}")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Notificações Discord
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from notify_utils import Product, DiscordEmbedBuilder
|
|
110
|
+
|
|
111
|
+
produto = Product(
|
|
112
|
+
product_id="PROD123",
|
|
113
|
+
name="Notebook Gamer",
|
|
114
|
+
url="https://loja.com/produto"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
builder = DiscordEmbedBuilder()
|
|
118
|
+
embed = builder.build_embed(produto, info, precos)
|
|
119
|
+
# Enviar via webhook Discord
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Documentação Completa
|
|
123
|
+
|
|
124
|
+
Para mais detalhes, consulte o arquivo [CLAUDE.md](CLAUDE.md) na raiz do projeto.
|
|
125
|
+
|
|
126
|
+
## Requisitos
|
|
127
|
+
|
|
128
|
+
- Python >= 3.12
|
|
129
|
+
- discord-webhook >= 1.4.1 (opcional, apenas para notificações)
|
|
130
|
+
|
|
131
|
+
## Licença
|
|
132
|
+
|
|
133
|
+
MIT
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# notify-utils
|
|
2
|
+
|
|
3
|
+
Biblioteca Python para parsing de preços de scraping, cálculo de descontos e análise estatística de histórico de preços.
|
|
4
|
+
|
|
5
|
+
## Funcionalidades
|
|
6
|
+
|
|
7
|
+
- **Parser de Preços**: Normaliza strings de preços de diferentes formatos (BR, US)
|
|
8
|
+
- **Cálculo de Descontos**: Detecta descontos reais vs anunciados usando histórico
|
|
9
|
+
- **Análise Estatística**: Média, mediana, tendências e volatilidade de preços
|
|
10
|
+
- **Validação de Preços**: Sistema inteligente para validar preços antes de adicionar ao histórico
|
|
11
|
+
- **Notificações Discord**: Envio de alertas de preço via webhook (opcional)
|
|
12
|
+
|
|
13
|
+
## Instalação
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install notify-utils
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Uso Básico
|
|
20
|
+
|
|
21
|
+
### Parsing de Preços
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from notify_utils import parse_price
|
|
25
|
+
|
|
26
|
+
preco = parse_price("R$ 1.299,90") # → 1299.90
|
|
27
|
+
preco = parse_price("$1,299.90") # → 1299.90
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Cálculo de Desconto com Histórico
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from notify_utils import Price, get_discount_info
|
|
34
|
+
from datetime import datetime, timedelta
|
|
35
|
+
|
|
36
|
+
# Histórico de preços
|
|
37
|
+
precos = [
|
|
38
|
+
Price(value=1299.90, date=datetime.now() - timedelta(days=60)),
|
|
39
|
+
Price(value=1199.90, date=datetime.now() - timedelta(days=30)),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Calcular desconto real baseado no histórico
|
|
43
|
+
info = get_discount_info(
|
|
44
|
+
current_price=899.90,
|
|
45
|
+
price_history=precos,
|
|
46
|
+
period_days=30
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
print(f"Desconto real: {info.discount_percentage:.2f}%")
|
|
50
|
+
print(f"É desconto real? {info.is_real_discount}")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Análise de Tendência
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from notify_utils import calculate_price_trend
|
|
57
|
+
|
|
58
|
+
trend = calculate_price_trend(precos, days=30)
|
|
59
|
+
|
|
60
|
+
print(f"Direção: {trend.direction}") # 'increasing', 'decreasing', 'stable'
|
|
61
|
+
print(f"Mudança: {trend.change_percentage:.2f}%")
|
|
62
|
+
print(f"Confiança: {trend.confidence}")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Validação de Preços
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from notify_utils import PriceHistory, Price, PriceAdditionStrategy
|
|
69
|
+
|
|
70
|
+
history = PriceHistory(product_id="PROD123", prices=precos)
|
|
71
|
+
|
|
72
|
+
# Validar antes de adicionar
|
|
73
|
+
novo_preco = Price(value=899.90, date=datetime.now())
|
|
74
|
+
result = history.add_price(
|
|
75
|
+
novo_preco,
|
|
76
|
+
strategy=PriceAdditionStrategy.SMART
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if result.action.value == "added":
|
|
80
|
+
print(f"Preço adicionado: R$ {result.affected_price.value:.2f}")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Notificações Discord
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from notify_utils import Product, DiscordEmbedBuilder
|
|
87
|
+
|
|
88
|
+
produto = Product(
|
|
89
|
+
product_id="PROD123",
|
|
90
|
+
name="Notebook Gamer",
|
|
91
|
+
url="https://loja.com/produto"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
builder = DiscordEmbedBuilder()
|
|
95
|
+
embed = builder.build_embed(produto, info, precos)
|
|
96
|
+
# Enviar via webhook Discord
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Documentação Completa
|
|
100
|
+
|
|
101
|
+
Para mais detalhes, consulte o arquivo [CLAUDE.md](CLAUDE.md) na raiz do projeto.
|
|
102
|
+
|
|
103
|
+
## Requisitos
|
|
104
|
+
|
|
105
|
+
- Python >= 3.12
|
|
106
|
+
- discord-webhook >= 1.4.1 (opcional, apenas para notificações)
|
|
107
|
+
|
|
108
|
+
## Licença
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
notify-utils
|
|
3
|
+
|
|
4
|
+
Biblioteca para parsing de preços, cálculo de descontos e análise estatística de histórico de preços.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .models import (
|
|
8
|
+
Product,
|
|
9
|
+
Price,
|
|
10
|
+
PriceHistory,
|
|
11
|
+
DiscountResult,
|
|
12
|
+
HistoryDiscountResult,
|
|
13
|
+
DiscountInfo,
|
|
14
|
+
PriceTrend,
|
|
15
|
+
PriceComparisonStatus,
|
|
16
|
+
PriceAction,
|
|
17
|
+
PriceAdditionStrategy,
|
|
18
|
+
PriceComparisonResult
|
|
19
|
+
)
|
|
20
|
+
from .parser import parse_price
|
|
21
|
+
from .discount import (
|
|
22
|
+
calculate_discount_percentage,
|
|
23
|
+
calculate_discount_absolute,
|
|
24
|
+
is_real_discount,
|
|
25
|
+
calculate_discount_from_history,
|
|
26
|
+
get_discount_info,
|
|
27
|
+
get_best_discount_from_history
|
|
28
|
+
)
|
|
29
|
+
from .statistics import (
|
|
30
|
+
calculate_mean,
|
|
31
|
+
calculate_median,
|
|
32
|
+
get_min_max,
|
|
33
|
+
filter_by_period,
|
|
34
|
+
filter_by_period_range,
|
|
35
|
+
calculate_volatility,
|
|
36
|
+
calculate_price_trend,
|
|
37
|
+
calculate_price_statistics,
|
|
38
|
+
days_since_most_recent_price,
|
|
39
|
+
get_recommended_period
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Import de notifiers
|
|
43
|
+
from .notifiers import (
|
|
44
|
+
DiscordEmbedBuilder,
|
|
45
|
+
format_price,
|
|
46
|
+
format_price_history,
|
|
47
|
+
get_unique_prices
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__version__ = "0.0.1"
|
|
51
|
+
__all__ = [
|
|
52
|
+
# Models
|
|
53
|
+
"Product",
|
|
54
|
+
"Price",
|
|
55
|
+
"PriceHistory",
|
|
56
|
+
"DiscountResult",
|
|
57
|
+
"HistoryDiscountResult",
|
|
58
|
+
"DiscountInfo",
|
|
59
|
+
"PriceTrend",
|
|
60
|
+
# Price Comparison
|
|
61
|
+
"PriceComparisonStatus",
|
|
62
|
+
"PriceAction",
|
|
63
|
+
"PriceAdditionStrategy",
|
|
64
|
+
"PriceComparisonResult",
|
|
65
|
+
# Parser
|
|
66
|
+
"parse_price",
|
|
67
|
+
# Discount functions
|
|
68
|
+
"calculate_discount_percentage",
|
|
69
|
+
"calculate_discount_absolute",
|
|
70
|
+
"is_real_discount",
|
|
71
|
+
"calculate_discount_from_history",
|
|
72
|
+
"get_discount_info",
|
|
73
|
+
"get_best_discount_from_history",
|
|
74
|
+
# Statistics
|
|
75
|
+
"calculate_mean",
|
|
76
|
+
"calculate_median",
|
|
77
|
+
"get_min_max",
|
|
78
|
+
"filter_by_period",
|
|
79
|
+
"filter_by_period_range",
|
|
80
|
+
"calculate_volatility",
|
|
81
|
+
"calculate_price_trend",
|
|
82
|
+
"calculate_price_statistics",
|
|
83
|
+
"days_since_most_recent_price",
|
|
84
|
+
"get_recommended_period",
|
|
85
|
+
# Notifiers (opcionais)
|
|
86
|
+
"DiscordEmbedBuilder",
|
|
87
|
+
"format_price",
|
|
88
|
+
"format_price_history",
|
|
89
|
+
"get_unique_prices",
|
|
90
|
+
]
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cálculos de desconto e validação de promoções.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, List
|
|
6
|
+
from .models import Price, PriceHistory, HistoryDiscountResult, DiscountInfo
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def calculate_discount_percentage(old_price: float, new_price: float) -> float:
|
|
10
|
+
"""
|
|
11
|
+
Calcula o percentual de desconto entre dois preços.
|
|
12
|
+
|
|
13
|
+
Formula: ((preço_antigo - preço_novo) / preço_antigo) * 100
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
old_price: Preço anterior (original)
|
|
17
|
+
new_price: Preço atual (com desconto)
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Percentual de desconto (valores positivos indicam desconto, negativos indicam aumento)
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ValueError: Se algum preço for negativo ou preço antigo for zero
|
|
24
|
+
"""
|
|
25
|
+
if old_price < 0 or new_price < 0:
|
|
26
|
+
raise ValueError("Preços não podem ser negativos")
|
|
27
|
+
|
|
28
|
+
if old_price == 0:
|
|
29
|
+
raise ValueError("Preço antigo não pode ser zero")
|
|
30
|
+
|
|
31
|
+
discount = ((old_price - new_price) / old_price) * 100
|
|
32
|
+
return round(discount, 2)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def calculate_discount_absolute(old_price: float, new_price: float) -> float:
|
|
36
|
+
"""
|
|
37
|
+
Calcula o valor absoluto do desconto.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
old_price: Preço anterior
|
|
41
|
+
new_price: Preço atual
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Diferença entre os preços (positivo = desconto, negativo = aumento)
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: Se algum preço for negativo
|
|
48
|
+
"""
|
|
49
|
+
if old_price < 0 or new_price < 0:
|
|
50
|
+
raise ValueError("Preços não podem ser negativos")
|
|
51
|
+
|
|
52
|
+
return round(old_price - new_price, 2)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_real_discount(
|
|
56
|
+
current_price: float,
|
|
57
|
+
advertised_old_price: Optional[float] = None,
|
|
58
|
+
price_history: Optional[List[Price]] = None,
|
|
59
|
+
threshold_percentage: float = 5.0
|
|
60
|
+
) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Verifica se um desconto anunciado é real.
|
|
63
|
+
|
|
64
|
+
Estratégia:
|
|
65
|
+
- COM histórico: Ignora advertised_old_price, compara preço atual com média histórica
|
|
66
|
+
- SEM histórico: Usa advertised_old_price como referência (primeira vez)
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
current_price: Preço atual anunciado
|
|
70
|
+
advertised_old_price: Preço "de" anunciado pela loja (usado apenas sem histórico)
|
|
71
|
+
price_history: Lista de preços históricos (opcional, mas prioritário)
|
|
72
|
+
threshold_percentage: Percentual mínimo para considerar desconto real (padrão: 5%)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True se o desconto for considerado real, False caso contrário
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
ValueError: Se não houver histórico nem advertised_old_price
|
|
79
|
+
"""
|
|
80
|
+
# Se houver histórico, IGNORA advertised_old_price e usa apenas histórico
|
|
81
|
+
if price_history and len(price_history) > 0:
|
|
82
|
+
# Calcula a média dos preços históricos
|
|
83
|
+
avg_price = sum(p.value for p in price_history) / len(price_history)
|
|
84
|
+
|
|
85
|
+
# Calcula o desconto em relação à média histórica
|
|
86
|
+
discount_pct = ((avg_price - current_price) / avg_price) * 100
|
|
87
|
+
|
|
88
|
+
# Considera desconto real se for >= threshold_percentage
|
|
89
|
+
return discount_pct >= threshold_percentage
|
|
90
|
+
|
|
91
|
+
# Sem histórico: usa advertised_old_price (fallback para primeira vez)
|
|
92
|
+
if advertised_old_price is None:
|
|
93
|
+
raise ValueError("É necessário fornecer advertised_old_price ou price_history")
|
|
94
|
+
|
|
95
|
+
# Verifica se há desconto básico
|
|
96
|
+
return current_price < advertised_old_price
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def calculate_discount_from_history(
|
|
100
|
+
current_price: float,
|
|
101
|
+
price_history: List[Price],
|
|
102
|
+
period_days: Optional[int] = 30,
|
|
103
|
+
use_median: bool = False,
|
|
104
|
+
skip_recent_days: int = 0
|
|
105
|
+
) -> Optional[HistoryDiscountResult]:
|
|
106
|
+
"""
|
|
107
|
+
Calcula desconto baseado no histórico de preços (ignora preço "de" anunciado).
|
|
108
|
+
|
|
109
|
+
Compara o preço atual com a média (ou mediana) do histórico de um período.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
current_price: Preço atual do produto
|
|
113
|
+
price_history: Lista de preços históricos
|
|
114
|
+
period_days: Período em dias para calcular a referência (None = todos os preços)
|
|
115
|
+
use_median: Se True usa mediana, se False usa média (padrão)
|
|
116
|
+
skip_recent_days: Ignora os N dias mais recentes (útil para evitar ruído, padrão: 0)
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
HistoryDiscountResult com informações do desconto ou None se não houver histórico suficiente
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
>>> # Ignora preços de 0-2 dias, usa apenas 3-30 dias
|
|
123
|
+
>>> result = calculate_discount_from_history(
|
|
124
|
+
... current_price=899.90,
|
|
125
|
+
... price_history=prices,
|
|
126
|
+
... period_days=30,
|
|
127
|
+
... skip_recent_days=3
|
|
128
|
+
... )
|
|
129
|
+
"""
|
|
130
|
+
if not price_history or len(price_history) == 0:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
# Importa funções de statistics
|
|
134
|
+
from .statistics import filter_by_period, filter_by_period_range, calculate_mean, calculate_median
|
|
135
|
+
|
|
136
|
+
# Filtra por período
|
|
137
|
+
if skip_recent_days > 0 and period_days:
|
|
138
|
+
# Usa intervalo: ignora dias recentes
|
|
139
|
+
filtered_prices = filter_by_period_range(price_history, skip_recent_days, period_days)
|
|
140
|
+
elif period_days:
|
|
141
|
+
# Comportamento padrão: de hoje até period_days
|
|
142
|
+
filtered_prices = filter_by_period(price_history, period_days)
|
|
143
|
+
else:
|
|
144
|
+
# Sem filtro de período
|
|
145
|
+
filtered_prices = price_history
|
|
146
|
+
|
|
147
|
+
if not filtered_prices:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
# Calcula preço de referência (média ou mediana)
|
|
151
|
+
if use_median:
|
|
152
|
+
reference_price = calculate_median(filtered_prices)
|
|
153
|
+
method = 'median'
|
|
154
|
+
else:
|
|
155
|
+
reference_price = calculate_mean(filtered_prices)
|
|
156
|
+
method = 'mean'
|
|
157
|
+
|
|
158
|
+
if reference_price is None:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# Calcula desconto
|
|
162
|
+
discount_pct = calculate_discount_percentage(reference_price, current_price)
|
|
163
|
+
discount_abs = calculate_discount_absolute(reference_price, current_price)
|
|
164
|
+
|
|
165
|
+
return HistoryDiscountResult(
|
|
166
|
+
current_price=current_price,
|
|
167
|
+
reference_price=reference_price,
|
|
168
|
+
discount_percentage=discount_pct,
|
|
169
|
+
discount_absolute=discount_abs,
|
|
170
|
+
is_real_discount=discount_pct > 0, # Positivo = desconto real
|
|
171
|
+
calculation_method=method,
|
|
172
|
+
period_days=period_days,
|
|
173
|
+
samples_count=len(filtered_prices)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_discount_info(
|
|
178
|
+
current_price: float,
|
|
179
|
+
price_history: Optional[List[Price]] = None,
|
|
180
|
+
advertised_old_price: Optional[float] = None,
|
|
181
|
+
period_days: int = 30,
|
|
182
|
+
use_median: bool = False,
|
|
183
|
+
auto_adjust_period: bool = True,
|
|
184
|
+
skip_recent_days: int = 0
|
|
185
|
+
) -> DiscountInfo:
|
|
186
|
+
"""
|
|
187
|
+
Função inteligente que calcula desconto automaticamente escolhendo a melhor estratégia.
|
|
188
|
+
|
|
189
|
+
Estratégia:
|
|
190
|
+
- COM histórico: Usa média/mediana do histórico (ignora advertised_old_price)
|
|
191
|
+
- SEM histórico: Usa advertised_old_price (primeira vez)
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
current_price: Preço atual do produto
|
|
195
|
+
price_history: Lista de preços históricos (opcional, mas prioritário)
|
|
196
|
+
advertised_old_price: Preço "de" anunciado (usado apenas sem histórico)
|
|
197
|
+
period_days: Período para cálculo da média histórica (padrão: 30 dias)
|
|
198
|
+
use_median: Se True usa mediana, se False usa média
|
|
199
|
+
auto_adjust_period: Se True, ajusta period_days automaticamente para incluir
|
|
200
|
+
o histórico mais recente (recomendado: True)
|
|
201
|
+
skip_recent_days: Ignora os N dias mais recentes do histórico (padrão: 0)
|
|
202
|
+
Útil para evitar ruído de dados muito recentes
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
DiscountInfo com informações completas do desconto
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
ValueError: Se não houver histórico nem advertised_old_price
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
>>> # Histórico de 33 dias atrás, period_days=30
|
|
212
|
+
>>> prices = [Price(1299.90, datetime.now() - timedelta(days=33))]
|
|
213
|
+
>>> info = get_discount_info(899.90, prices, period_days=30, auto_adjust_period=True)
|
|
214
|
+
>>> info.period_days # 30 (solicitado)
|
|
215
|
+
>>> info.adjusted_period_days # 33 (ajustado)
|
|
216
|
+
>>> info.days_since_most_recent # 33
|
|
217
|
+
|
|
218
|
+
>>> # Ignorar preços de 0-2 dias, usar apenas 3-30 dias
|
|
219
|
+
>>> info = get_discount_info(899.90, prices, period_days=30, skip_recent_days=3)
|
|
220
|
+
>>> info.skip_recent_days # 3
|
|
221
|
+
"""
|
|
222
|
+
# Estratégia 1: COM histórico - usa apenas histórico
|
|
223
|
+
if price_history and len(price_history) > 0:
|
|
224
|
+
# Importa funções de statistics
|
|
225
|
+
from .statistics import days_since_most_recent_price, get_recommended_period
|
|
226
|
+
|
|
227
|
+
# Calcula dias desde o mais recente
|
|
228
|
+
days_since = days_since_most_recent_price(price_history)
|
|
229
|
+
|
|
230
|
+
# Ajusta período se necessário
|
|
231
|
+
actual_period = period_days
|
|
232
|
+
if auto_adjust_period:
|
|
233
|
+
actual_period = get_recommended_period(price_history, period_days)
|
|
234
|
+
|
|
235
|
+
history_discount = calculate_discount_from_history(
|
|
236
|
+
current_price=current_price,
|
|
237
|
+
price_history=price_history,
|
|
238
|
+
period_days=actual_period,
|
|
239
|
+
use_median=use_median,
|
|
240
|
+
skip_recent_days=skip_recent_days
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if history_discount:
|
|
244
|
+
return DiscountInfo(
|
|
245
|
+
current_price=history_discount.current_price,
|
|
246
|
+
reference_price=history_discount.reference_price,
|
|
247
|
+
discount_percentage=history_discount.discount_percentage,
|
|
248
|
+
discount_absolute=history_discount.discount_absolute,
|
|
249
|
+
is_real_discount=history_discount.is_real_discount,
|
|
250
|
+
strategy='history',
|
|
251
|
+
has_history=True,
|
|
252
|
+
calculation_method=history_discount.calculation_method,
|
|
253
|
+
period_days=period_days, # Período solicitado
|
|
254
|
+
samples_count=history_discount.samples_count,
|
|
255
|
+
adjusted_period_days=actual_period if auto_adjust_period else None,
|
|
256
|
+
days_since_most_recent=days_since,
|
|
257
|
+
skip_recent_days=skip_recent_days if skip_recent_days > 0 else None
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
# Se nenhum preço no período, mas temos histórico, use todos os preços
|
|
261
|
+
history_discount = calculate_discount_from_history(
|
|
262
|
+
current_price=current_price,
|
|
263
|
+
price_history=price_history,
|
|
264
|
+
period_days=None, # Usa todos os preços disponíveis
|
|
265
|
+
use_median=use_median,
|
|
266
|
+
skip_recent_days=0 # Não pula dias quando usa todos
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if history_discount:
|
|
270
|
+
return DiscountInfo(
|
|
271
|
+
current_price=history_discount.current_price,
|
|
272
|
+
reference_price=history_discount.reference_price,
|
|
273
|
+
discount_percentage=history_discount.discount_percentage,
|
|
274
|
+
discount_absolute=history_discount.discount_absolute,
|
|
275
|
+
is_real_discount=history_discount.is_real_discount,
|
|
276
|
+
strategy='history',
|
|
277
|
+
has_history=True,
|
|
278
|
+
calculation_method=history_discount.calculation_method,
|
|
279
|
+
period_days=period_days, # Período solicitado originalmente
|
|
280
|
+
samples_count=history_discount.samples_count,
|
|
281
|
+
adjusted_period_days=None, # Usou todos os preços, não apenas período
|
|
282
|
+
days_since_most_recent=days_since,
|
|
283
|
+
skip_recent_days=None # Fallback não usa skip
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Estratégia 2: SEM histórico - usa advertised_old_price
|
|
287
|
+
if advertised_old_price is None:
|
|
288
|
+
raise ValueError("É necessário fornecer price_history ou advertised_old_price")
|
|
289
|
+
|
|
290
|
+
discount_pct = calculate_discount_percentage(advertised_old_price, current_price)
|
|
291
|
+
discount_abs = calculate_discount_absolute(advertised_old_price, current_price)
|
|
292
|
+
|
|
293
|
+
return DiscountInfo(
|
|
294
|
+
current_price=current_price,
|
|
295
|
+
reference_price=advertised_old_price,
|
|
296
|
+
discount_percentage=discount_pct,
|
|
297
|
+
discount_absolute=discount_abs,
|
|
298
|
+
is_real_discount=discount_pct > 0, # Assume verdadeiro na primeira vez
|
|
299
|
+
strategy='advertised',
|
|
300
|
+
has_history=False,
|
|
301
|
+
calculation_method='advertised',
|
|
302
|
+
period_days=None,
|
|
303
|
+
samples_count=None,
|
|
304
|
+
adjusted_period_days=None,
|
|
305
|
+
days_since_most_recent=None,
|
|
306
|
+
skip_recent_days=None
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def get_best_discount_from_history(price_history: PriceHistory) -> Optional[HistoryDiscountResult]:
|
|
311
|
+
"""
|
|
312
|
+
Encontra o maior desconto válido comparando o preço atual com a média histórica.
|
|
313
|
+
|
|
314
|
+
DEPRECATED: Use calculate_discount_from_history() ou get_discount_info()
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
price_history: Histórico de preços do produto
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
HistoryDiscountResult com informações do melhor desconto ou None se não houver desconto
|
|
321
|
+
"""
|
|
322
|
+
current = price_history.get_current_price()
|
|
323
|
+
|
|
324
|
+
if not current or len(price_history.prices) < 2:
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
# Usa calculate_discount_from_history internamente
|
|
328
|
+
historical_prices = price_history.prices[1:] # Exclui o preço atual
|
|
329
|
+
|
|
330
|
+
return calculate_discount_from_history(
|
|
331
|
+
current_price=current.value,
|
|
332
|
+
price_history=historical_prices,
|
|
333
|
+
period_days=None, # Usa todos os preços históricos
|
|
334
|
+
use_median=False # Usa média
|
|
335
|
+
)
|