#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2015 Chintalagiri Shashank
#
# This file is part of tendril.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
TI Vendor Module (:mod:`tendril.sourcing.ti`)
=============================================
"""
import locale
import re
import urllib
import urlparse
import HTMLParser
import traceback
from .vendors import VendorBase
from .vendors import VendorElnPartBase
from .vendors import VendorPrice
from .vendors import SearchResult
from .vendors import SearchPart
from tendril.utils import www
from tendril.conventions.electronics import parse_ident
from tendril.utils import log
logger = log.get_logger(__name__, log.DEFAULT)
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
html_parser = HTMLParser.HTMLParser()
[docs]class TIElnPart(VendorElnPartBase):
def __init__(self, tipartno, ident=None, vendor=None, max_age=600000):
if not tipartno:
logger.error("Not enough information to create a TI Part")
if vendor is None:
vendor = dvobj
self.url_base = vendor.url_base
super(TIElnPart, self).__init__(tipartno, ident, vendor, max_age)
[docs] def _get_data(self):
soup = self._get_product_soup()
for price in self._get_prices(soup):
self.add_price(price)
self.mpartno = self._get_mpartno()
self.manufacturer = self._get_manufacturer()
self.datasheet = self._get_datasheet_link()
self.vpartdesc = self._get_description(soup)
self.package = self._get_package(soup)
@property
def vpart_url(self):
return urlparse.urljoin(
self.url_base,
"Search.aspx?k={0}&pt=-1".format(urllib.quote_plus(self.vpno))
)
[docs] def _get_product_soup(self):
start_url = self.vpart_url
soup = www.get_soup(start_url)
ptable = soup.find('table',
id=re.compile(r'ctl00_ProductList'))
if ptable is None:
raise ValueError("No result list founds for " + self.vpno)
rows = ptable.find_all('td', class_=re.compile(r'tableNode'))
if not rows:
raise ValueError("No results founds for " + self.vpno)
product_url = None
for row in rows:
product_link = row.find('a', class_='highlight')
pno = product_link.text.strip()
if pno.strip() == self.vpno.strip():
product_url = urlparse.urljoin(self.url_base,
product_link.attrs['href'])
break
if not product_url:
raise ValueError("Exact match not found for " + self.vpno)
soup = www.get_soup(product_url)
if soup is None:
logger.error("Unable to open TI product page : " + self.vpno)
return
return soup
rex_price = re.compile(ur'^((?P<start>\d+)\s+-\s+)?(?P<end>\d+)$')
[docs] def _get_prices(self, soup):
ptable = soup.find('div',
id='ctl00_ctl00_NestedMaster_PageContent'
'_ctl00_BuyProductDialog1_tierPricing')
prices = []
if ptable is None:
self.vqtyavail = 0
return []
rows = ptable.find_all('tr')
availq = None
for row in rows:
cells = row.findAll('td')
if not len(cells):
continue
price_cell = row.find('span', id=re.compile(r'_Price$'))
price_text = price_cell.text.strip()
price = locale.atof(price_text.replace('$', ''))
qty_cell = row.find('span', id=re.compile(r'_Range_From$'))
qty_text = qty_cell.text.strip()
m = self.rex_price.match(qty_text)
if not m:
raise ValueError(
"Didn't get a qty from " + qty_text + " for " + self.vpno
)
try:
moq = locale.atoi(m.group('start'))
except AttributeError:
moq = locale.atoi(m.group('end'))
maxq = locale.atoi(m.group('end'))
if not availq or maxq > availq:
availq = maxq
price_obj = VendorPrice(moq, price, self._vendor.currency,
oqmultiple=1)
prices.append(price_obj)
self.vqtyavail = availq
return prices
[docs] def _get_mpartno(self):
return self.vpno
@staticmethod
[docs] def _get_manufacturer():
return 'Texas Instruments'
@staticmethod
[docs] def _get_package(soup):
pack = soup.find('span', id=re.compile('ctl00_Package')).text.strip()
pin = soup.find('span', id=re.compile('ctl00_PIN')).text.strip()
package = '-'.join([pack, pin])
return package
@staticmethod
[docs] def _get_datasheet_link():
return None
@staticmethod
[docs] def _get_description(soup):
desc_cell = soup.find('tr', id=re.compile('trSku'))
strings = []
for string in desc_cell.stripped_strings:
strings.append(string)
return www.strencode(strings[1])
[docs]class VendorTI(VendorBase):
_partclass = TIElnPart
_vendorlogo = '/static/images/vendor-logo-ti.png'
_url_base = 'https://store.ti.com'
_devices = ['IC SMD', 'IC THRU', 'IC PLCC',]
_type = 'TI'
def __init__(self, name, dname, pclass, mappath=None,
currency_code='USD', currency_symbol='US$', **kwargs):
self._searchpages_filters = {}
super(VendorTI, self).__init__(name, dname, pclass, mappath,
currency_code, currency_symbol,
**kwargs)
self.add_order_baseprice_component("Shipping Cost", 7)
self.add_order_additional_cost_component("Customs", 12.85)
[docs] def search_vpnos(self, ident):
device, value, footprint = parse_ident(ident)
if device not in self._devices:
return None, 'NODEVICE'
try:
return self._get_search_vpnos(device, value, footprint)
except Exception:
logger.error(traceback.format_exc())
logger.error('Fatal Error searching for : ' + ident)
return None, None
@staticmethod
[docs] def _search_preprocess(device, value, footprint):
if footprint in ['TO220', 'TO-220']:
footprint = 'TO-220-3'
elif footprint in ['TO92', 'TO-92']:
footprint = 'TO-92-3'
return device, value, footprint
_package_norms = [
# --- Standard Cases --- #
# TO-220-3 (Normalized)
# http://www.ti.com/sc/docs/psheets/type/to_220.html
# KC-3
# KC-5
# KC-7
# KCS-3
# KCT-3
# KV-11
# KV-5
# KV-7
# KVT-7
# NDA-11
# NDB-15
# NDD-3
# NDE-3
# NDF-3
# NDG-3
# NDH-5
# NDK-11
# NDL-15
# NDZ-7
# NEB03A-3
# NEB03F-3
# NEB03G-3
# NEB05B-5
# NEB05E-5
# NEB05F-5
# NEK-15
(re.compile(ur'^((K((C[ST]?)|(VT?)))|(ND[ABDEFGHKLZ])|(NEB0((3[AFG])|(5[BEF])))|(NEK))-(?P<pinc>\d+)$'), # noqa
'TO-220-{0}', ['pinc']),
]
[docs] def _standardize_package(self, package):
for norm in self._package_norms:
m = norm[0].match(package)
if m:
params = [m.group(var) for var in norm[2]]
package = norm[1].format(*params)
return package
[docs] def _process_resultpage_row(self, row):
product_link = row.find('a', class_='highlight')
pno = product_link.text.strip()
try:
product = TIElnPart(pno, vendor=self)
except:
return None
if product.vqtyavail > 0:
ns = False
bprice = product.prices[0]
unitp = bprice.unit_price
minqty = bprice.moq
else:
ns = True
unitp = None
minqty = None
mfgpno = pno
if product.package is None:
return None
package = self._standardize_package(product.package)
return SearchPart(pno, mfgpno, package, ns, unitp, minqty, row)
[docs] def _process_search_soup(self, soup):
ptable = soup.find('table',
id=re.compile(r'ctl00_ProductList'))
if ptable is None:
return SearchResult(False, None, 'NO_RESULTS:3')
rows = ptable.find_all('td', class_=re.compile(r'tableNode'))
if not rows:
return SearchResult(False, None, 'NO_RESULTS:1')
parts = []
for row in rows:
part = self._process_resultpage_row(row)
if isinstance(part, SearchPart):
parts.append(part)
return SearchResult(True, parts, '')
@staticmethod
[docs] def _prefilter_parts(parts, value):
parts = [part for part in parts if value.upper() in part.mfgpno]
return parts
@staticmethod
[docs] def _get_search_soups(soup):
yield soup
[docs] def _get_search_vpnos(self, device, value, footprint):
if value.strip() == '':
return None, 'NOVALUE'
device, value, footprint = \
self._search_preprocess(device, value, footprint)
url = urlparse.urljoin(
self._url_base,
"Search.aspx?k={0}&pt=-1".format(urllib.quote_plus(value))
)
soup = www.get_soup(url)
if soup is None:
return None, 'URL_FAIL'
parts = []
strategy = ''
for soup in self._get_search_soups(soup):
sr = self._process_search_soup(soup)
if sr.success is True:
if sr.parts:
parts.extend(sr.parts)
strategy += ', ' + sr.strategy
strategy = '.' + strategy
if not len(parts):
return None, strategy + ':NO_RESULTS:COLLECTED'
parts = self._prefilter_parts(parts, value)
if not len(parts):
return None, strategy + ':NO_RESULTS:PREFILTER'
sr = self._filter_results(parts, value, footprint)
if sr.parts:
pnos = list(set(sr.parts))
pnos = map(lambda x: html_parser.unescape(x), pnos)
return pnos, ':'.join([strategy, sr.strategy])
return None, strategy + ':NO_RESULTS:POSTFILTER'
dvobj = VendorTI('ti', 'transient', 'electronics',
currency_code='USD', currency_symbol='US$')