Update to new RevBank v6 metadata format

This commit is contained in:
polyfloyd 2024-01-20 20:32:48 +01:00
parent 8a9c7113f8
commit 1abc17d8e9
2 changed files with 84 additions and 71 deletions

View file

@ -39,34 +39,29 @@ WantedBy=timers.target
``` ```
## Configuratie ## Configuratie
De metadata voor producten scrapen is afhankelijk van een patch voor RevBank die niet upstream is en Inflatinator vereist minimaal versie 6.0 van RevBank. Deze versie introduceert een metadataformaat
er ook niet gaat komen in de huidige vorm. waar Inflatinator gebruik van maakt.
Zie: https://github.com/revspace/revbank/pull/18
Er is wel de intentie om dit upstream te laten werken, maar niet in de huidige vorm. Wanneer je Het is vereist om de productbeschrijving te quoten. Metadata komt aan het einde van een regel en is
Inflatinator bijwerkt is het handig om te controlleren of je de configuratie moet bijwerken. in `#<key>=<value>` formaat, waar de `=<value>` optioneel is.
De scrapemetadata komt aan het einde van een productregel in commentaarm et een `#`. Inflatinator Inflatinator zal regels herschrijven met nieuwe prijzen, beschrijvingen en eventueel nieuwe
zal regels herschrijven met nieuwe prijzen en producttitels en eventueel nieuwe barcodes. Aliassen barcodes. Aliassen en barcodes die je zelf toegevoegd blijven staan.
en barcodes die je zelf toegevoegd blijven staan.
### Albert Heijn ### Albert Heijn
Zie hier onder voorbeelden van de metadata die toegevoegd moet worden per product: Zie hier onder voorbeelden van de metadata die toegevoegd moet worden per product:
``` ```
8711327538481 0.80 Ola Liuk # ah:wi162664 8x 8711327538481 0.80 "Ola Liuk" #ah=wi162664 #qty=8
8712100340666 0.45 Ola Raket # ah:wi209562 12x 8712100340666 0.45 "Ola Raket" #ah=wi209562 #qty=12
``` ```
De `wi162664` is de SKU van hoe het product heet op de website van de AH, je vind deze in de URL De `wi162664` is de SKU van hoe het product heet op de website van de AH, je vind deze in de URL
van de productpagina. van de productpagina.
De `8x` daar achteraan is het aantal individuele producten per verpakking. Dit is niet heel De `qty=8` daar achteraan is het aantal individuele producten per verpakking. Dit is niet heel
betrouwbaar terug te vinden op de pagina, dus je zult het zelf moeten opzoeken. betrouwbaar terug te vinden op de pagina, dus je zult het zelf moeten opzoeken.
Het is valide om alleen de metadata op een regel te hebben om mee te starten, Inflatinator zal zelf
de barcode, prijs en titel aanvullen.
### Sligro ### Sligro
Het verkrijgen van de prijzen van de Sligro vereist een account. Je configureert deze met Het verkrijgen van de prijzen van de Sligro vereist een account. Je configureert deze met
@ -78,11 +73,11 @@ SLIGRO_PASSWORD=<wachtwoord>
Sligro producten zien er zo uit: Sligro producten zien er zo uit:
``` ```
5000112659184,5000112658873 0.95 Coca-Cola Cola Zero Sugar (33 cl) # sligro 5000112659184,5000112658873 0.95 "Coca-Cola Cola Zero Sugar (33 cl)" #sligro
4011100240216,40111216 0.80 Bounty Kokos Melk Chocolade Singles (57 gram) # sligro 4011100240216,40111216 0.80 "Bounty Kokos Melk Chocolade Singles (57 gram)" #sligro
``` ```
Alleen een `# sligro` aan het einde van de regel is voldoende, Inflatinator gebruikt de **eerste** Alleen een `#sligro` aan het einde van de regel is voldoende, Inflatinator gebruikt de **eerste**
barcode om het product te vinden op de website. barcode om het product te vinden op de website.
Verpakkingen van de Sligro hebben over het algemeen producten hier in zitten die een andere Verpakkingen van de Sligro hebben over het algemeen producten hier in zitten die een andere

View file

@ -1,91 +1,109 @@
from dataclasses import dataclass
from decimal import Decimal, ROUND_UP from decimal import Decimal, ROUND_UP
from typing import Dict, Optional, List
import logging import logging
import re import re
import scrapers import scrapers
import shlex
profit_margin = Decimal('1.3') profit_margin = Decimal('1.3')
class AutoUpdate: @dataclass
_ah_meta_re = re.compile(r'#\s*ah:(?P<sku>\S+)\s+(?P<units>\d+)x$') class Product:
_sligro_meta_re = re.compile(r'^(?P<gtin13>\d{13})[^#]+#\s*sligro$') aliases: List[str]
price: Decimal
def __init__(self, vendor, sku, units): description: str
self.vendor = vendor metadata: Dict[str, Optional[str]]
self.sku = sku
self.units = units
def __str__(self):
if self.vendor == 'sligro':
return f'{self.vendor}'
if self.units:
return f'{self.vendor}:{self.sku} {self.units}x'
return f'{self.vendor}:{self.sku}'
@staticmethod @staticmethod
def from_product_line(line): def from_line(line: str) -> "Product":
ah = AutoUpdate._ah_meta_re.search(line) if not line.strip():
if ah: raise Exception('line is empty')
return AutoUpdate('ah', ah['sku'], int(ah['units'])) if line.startswith('#'):
raise Exception('line is a comment')
sligro = AutoUpdate._sligro_meta_re.search(line) fields = shlex.split(line)
if sligro: aliases = fields[0].split(',')
return AutoUpdate('sligro', sligro['gtin13'], None) price = Decimal(fields[1])
description = fields[2]
# TODO: support addons
raise Exception('no auto update directive found') metadata = {}
for f in fields:
if f.startswith('#'):
s = f.lstrip('#').split('=')
(k, v) = (s[0], None) if len(s) == 1 else s
metadata[k] = v
assert AutoUpdate.from_product_line('# ah:wi162664 8x') return Product(
assert AutoUpdate.from_product_line('8711327538481,liuk 0.80 Ola Liuk # ah:wi162664 8x') aliases=aliases,
assert AutoUpdate.from_product_line('5000112659184 # sligro') price=price,
assert AutoUpdate.from_product_line('5000112659184 1.00 Cola Zero # sligro') description=description,
assert AutoUpdate.from_product_line('5000112659184,colazero 1.00 Cola Zero # sligro') metadata=metadata,
)
def format_line(self):
aliases = ','.join(self.aliases)
price = f'{self.price:.2f}'
description = f'"{self.description}"'
metadata = ' '.join(sorted(f'#{k}' if v is None else f'#{k}={v}' for (k, v) in self.metadata.items()))
return f'{aliases:<30} {price:<6} {description:<60} {metadata}'
def find_product_details(auto_update): assert Product.from_line('8711327538481,liuk 0.80 "Ola Liuk" #ah=wi162664 #qty=8') == \
if auto_update.vendor == 'ah': Product(['8711327538481','liuk'], Decimal('0.8'), 'Ola Liuk', {'ah': 'wi162664', 'qty': '8'})
return scrapers.ah_get_by_sku(auto_update.sku, auto_update.units) assert Product.from_line('5000112659184,colazero 1.00 "Cola Zero" #sligro') == \
if auto_update.vendor == 'sligro': Product(['5000112659184','colazero'], Decimal(1), 'Cola Zero', {'sligro': None})
return scrapers.sligro_get_by_gtin(auto_update.sku) assert Product.from_line('8711327538481,liuk 0.80 "Ola Liuk" #ah=wi162664 #qty=8').format_line() == \
raise Exception(f'unknown vendor: {auto_update.vendor}') '8711327538481,liuk 0.80 "Ola Liuk" #ah=wi162664 #qty=8'
assert Product(['5000112659184','colazero'], Decimal(1), 'Cola Zero', {'sligro': None}).format_line() == \
'5000112659184,colazero 1.00 "Cola Zero" #sligro'
class NoAutoUpdate(Exception):
def __init__(self):
super().__init__('no auto update directive')
def find_product_details(product: Product):
if (ah_sku := product.metadata.get('ah', None)):
return scrapers.ah_get_by_sku(ah_sku, int(product.metadata['qty']))
if 'sligro' in product.metadata:
return scrapers.sligro_get_by_gtin(product.aliases[0])
raise NoAutoUpdate()
def update_product_pricings(src): def update_product_pricings(src):
find_aliases = re.compile(r'^(?P<aliases>\S+)')
lines = src.split('\n')
lines_out = [] lines_out = []
for line in src.split('\n'):
for line in lines:
try: try:
auto_update = AutoUpdate.from_product_line(line) product = Product.from_line(line)
logging.debug('Found updatable product: %s', auto_update)
except Exception as err: except Exception as err:
lines_out.append(line) lines_out.append(line)
continue continue
try: try:
prod_info = find_product_details(auto_update) prod_info = find_product_details(product)
except NoAutoUpdate:
logging.debug('no auto update: %s', product)
lines_out.append(line)
continue
except Exception as err: except Exception as err:
logging.error('could not update %s: %s', auto_update, err) logging.error('did not update %s: %s', product, err)
lines_out.append(line) lines_out.append(line)
continue continue
product_aliases = set() human_aliases = sorted(set(product.aliases) - set([prod_info.gtin]) - set(prod_info.aliases))
if not line.startswith('#'): product.aliases = [prod_info.gtin, *prod_info.aliases, *human_aliases]
human_aliases = set(find_aliases.search(line)['aliases'].split(','))
human_aliases -= set([prod_info.gtin])
human_aliases -= set(prod_info.aliases)
human_aliases = sorted(human_aliases)
scannables = ','.join([prod_info.gtin, *prod_info.aliases, *human_aliases])
# Apply profit margin and divide by the number of units per sold packaging. # Apply profit margin and divide by the number of units per sold packaging.
unit_price = prod_info.price * profit_margin / prod_info.units unit_price = prod_info.price * profit_margin / prod_info.units
# Round up to 5ct. # Round up to 5ct.
unit_price = (unit_price * 20).quantize(Decimal('1'), rounding=ROUND_UP) / 20 product.price = (unit_price * 20).quantize(Decimal('1'), rounding=ROUND_UP) / 20
fmt_price = f'{unit_price:.2f}' lines_out.append(product.format_line())
lines_out.append(f'{scannables:<30} {fmt_price:<6} {prod_info.name:<60} # {auto_update}')
logging.debug(f'Found "{prod_info.name}", buy €{prod_info.price/prod_info.units:.2f}, sell €{fmt_price}') logging.debug(f'Found "{prod_info.name}", buy €{prod_info.price/prod_info.units:.2f}, sell €{product.price:.2f}')
return '\n'.join(lines_out) return '\n'.join(lines_out)