import urllib.request import json import ssl import sys import re import io if sys.platform == 'win32': sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE endpoint = 'https://appwrite.almeira.com.co/v1' project_id = '69b84df5003bd8957c5c' api_key = 'standard_cce3cdb866ea6433888d40cedd24dc45987372308fdc3dd0697de08648c0e5c7e04b02c3bcee271403fd656b00a5820f04c08b51631284a6e8a3582d8aed2aa33234f3c29cc777e9b162eb54810b789251b0d006a9a3b081ebc2da53b9baa94614b124b3508ad849fd9a4187cc6094f506d4b57f409b69d9562a74df594f3ec3' db_id = 'sneakers_multistore_db' collection_id = 'catalog_products' headers = { 'X-Appwrite-Project': project_id, 'X-Appwrite-Key': api_key, 'Content-Type': 'application/json' } def clean_title(title, brand='Sneakers'): c = title or '' # 0. Normalizar fuentes estilizadas (Unicode Mathematical / Bold fonts) import unicodedata c = unicodedata.normalize('NFKD', c).strip() # 1. Quitar emojis y pictogramas de todo tipo c = re.sub(r'[\U00010000-\U0010ffff]|[\u2600-\u27bf]|[\u2300-\u23ff]|[\u200d\ufe0f]|\*|⭐|🌹|⚡|💥|👖|👩🏻|👫|👻|☀️|🔝|🙋🏻‍♀️|😍|🎩|💕|⏰|🌎', ' ', c) # 2. Quitar códigos de proveedor/bodega: Cod: 734-732 Bbo, Cod: 2236, Cod: 668/833 Bbo, etc. c = re.sub(r'\bcod(?:igo)?\s*[:\-–—]?\s*[\d\s\-\/]+(?:\s*bbo)?\b', ' ', c, flags=re.I) # 3. Quitar paréntesis o corchetes con tallas o números c = re.sub(r'\(\s*(?:tallas?|euro|eur|talla|disponibles?|del)?[^\)]*\b(?:3[2-9]|4[0-9]|5[0-6])\b[^\)]*\)', ' ', c, flags=re.I) c = re.sub(r'\([^\)]*\b(?:euro|eur|talla|tallas|mil)\b[^\)]*\)', ' ', c, flags=re.I) c = re.sub(r'\(\s*\)|\[\s*\]', ' ', c) # 4. Quitar 'del 36 al 39 eur', 'del 40 al 44 eur' c = re.sub(r'\bdel\s+(?:3[0-9]|4[0-9])\s+al\s+(?:3[0-9]|4[0-9])(?:\s*(?:eur|eu|euro))?\b', ' ', c, flags=re.I) # 5. Quitar 'euro 36 - 39', 'eur 36/37/38', 'eu 36 al 39', '36 al 39 eu' c = re.sub(r'\b(?:euro|eur|eu)\s*[:\-–—\/\s]+(?:3[0-9]|4[0-9])(?:\s*(?:-|–|—|a|al|\/)\s*(?:3[0-9]|4[0-9]))*', ' ', c, flags=re.I) c = re.sub(r'\b(?:3[0-9]|4[0-9])\s+al\s+(?:3[0-9]|4[0-9])(?:\s*(?:eur|eu|euro))\b', ' ', c, flags=re.I) # 6. Quitar secuencias de tallas: 32-34-36-38-40, 36/37/38, 36 - 38 - 44, 36 & Hombre 43 c = re.sub(r'\b(?:3[2-9]|4[0-9]|5[0-6])\s*(?:&|y|\/|\-)\s*(?:hombre\s+)?(?:3[2-9]|4[0-9]|5[0-6])(?:\s*(?:eur|eu|euro))?\b', ' ', c, flags=re.I) c = re.sub(r'\b(?:3[2-9]|4[0-9]|5[0-6])(?:\s*[-–—\/]\s*(?:3[2-9]|4[0-9]|5[0-6]))+\b', ' ', c, flags=re.I) # 7. Quitar rangos de tallas: 36 - 39, 40 - 41, 36 a 39, 36 al 39 c = re.sub(r'\b(?:3[4-9]|4[0-8])\s*(?:-|–|—|a|al|\/)\s*(?:3[4-9]|4[0-8])\b', ' ', c, flags=re.I) # 8. Quitar tallas individuales con palabra clave c = re.sub(r'\b(?:talla|tallas|eur|euro|eu)\s+(?:3[2-9]|4[0-8])\b', ' ', c, flags=re.I) c = re.sub(r'\b(?:3[2-9]|4[0-8])\s*(?:eur|euro|eu)\b', ' ', c, flags=re.I) c = re.sub(r'\btallas?\s+(?:de\s+)?(?:hombre|dama|disponibles?)?(?:\s+mil)?\b', ' ', c, flags=re.I) # 9. Quitar .000 y residuos de precios c = re.sub(r'(?:^|[\s\(\[\.,\-_])(?:[1-9]\d{0,2})?\.000\b', ' ', c, flags=re.I) c = re.sub(r'\b\d{2,3}[\.,]\d{3}\b|\b\d{2,3}k\b|\b\d{2,3}\s*mil\b|\$\s*\d+', ' ', c, flags=re.I) c = re.sub(r'\.000\b', ' ', c, flags=re.I) # 10. Quitar palabras residuales c = re.sub(r'dama\s*y\s*caballero|recarga|nueva\s*mercanc[ií]a|[uú]ltima\s*unidad', ' ', c, flags=re.I) c = re.sub(r'\b(costo|precio|valor|al\s*por\s*mayor|mayorista)\b', ' ', c, flags=re.I) c = re.sub(r'\b(hombre|dama|caballero)\b', ' ', c, flags=re.I) c = re.sub(r'\b(eur|euro|eu|mil)\b', ' ', c, flags=re.I) # 11. Limpiar símbolos residuales y ampersands aislados c = re.sub(r'\s+&\s*$', '', c) c = re.sub(r'^\s*&\s+', '', c) c = re.sub(r'^[#*_\-•—–~:;,\s\.\/&]+|[#*_\-•—–~:;,\s\.\/&]+$', '', c) c = re.sub(r'\s+', ' ', c).strip() # 12. Fallback si quedó genérico o vacío clean_lower = c.lower() is_generic = ( len(c) < 3 or re.match(r'^[\d\s\-.,/&]+$', c) or clean_lower in ('euro', 'eur', 'zapatillas', 'sneakers', 'calzado', 'estilo', 'moda', 'solo', 'sin producto') or clean_lower.startswith('solo ') or clean_lower.startswith('euro ') or clean_lower.startswith('sin producto') ) if is_generic: if brand and brand not in ('Urbano / Variado', 'Desconocida', 'Generica'): c = f'{brand} Urban Edition' else: c = 'Sneakers Urban Classic' # 13. Capital Case limpio conservando acrónimos words = c.split() formatted_words = [] for w in words: if re.match(r'^(OG|SE|SB|TN|CL|GT|RS-X|BB|LV|NB|XT-6|CR7|V2K|V5K|9060|550|530|1906R|2002R)$', w, re.I): formatted_words.append(w.upper()) elif re.match(r'^[A-Z0-9]+$', w) and len(w) <= 4 and not re.search(r'[aeiou]', w, re.I): formatted_words.append(w.upper()) else: formatted_words.append(w.capitalize()) return ' '.join(formatted_words) def appwrite_get(path): req = urllib.request.Request(f'{endpoint}{path}', headers=headers, method='GET') with urllib.request.urlopen(req, context=ctx, timeout=30) as resp: return json.loads(resp.read().decode()) def appwrite_patch(path, data): body = json.dumps(data).encode('utf-8') req = urllib.request.Request(f'{endpoint}{path}', data=body, headers=headers, method='PATCH') with urllib.request.urlopen(req, context=ctx, timeout=30) as resp: return json.loads(resp.read().decode()) print("📦 Consultando productos desde Appwrite...") docs = [] cursor = None while True: q = '?limit=100' if cursor: q += f'&queries[]=cursorAfter("{cursor}")' path = f'/databases/{db_id}/collections/{collection_id}/documents{q}' res = appwrite_get(path) cur_docs = res.get('documents', []) if not cur_docs: break docs.extend(cur_docs) cursor = cur_docs[-1]['$id'] if len(cur_docs) < 100: break print(f"Total documentos encontrados en Appwrite: {len(docs)}") updated_count = 0 for doc in docs: doc_id = doc['$id'] old_name = doc.get('name', '') brand = doc.get('brand', 'Sneakers') new_name = clean_title(old_name, brand) if old_name != new_name: try: appwrite_patch(f'/databases/{db_id}/collections/{collection_id}/documents/{doc_id}', { 'data': { 'name': new_name, 'modelName': new_name } }) print(f" [FIXED] {doc_id}:\n OLD: {old_name}\n NEW: {new_name}") updated_count += 1 except Exception as e: print(f" [ERROR] {doc_id}: {e}") print(f"🎉 Migración completada en Appwrite: {updated_count} productos actualizados.")