diff --git a/.gitignore b/.gitignore
index 6fde163..d3853ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
*.pyc
*.ini
*.log
+*.swp
=======
docs/_build
+build
+dist
+Admin_CFDI.egg-info
diff --git a/README.rst b/README.rst
index 9c40779..11787bc 100644
--- a/README.rst
+++ b/README.rst
@@ -2,13 +2,13 @@ admin-cfdi
==========
:Autores:
- Universo Libre A.C., Python Cabal
+ Ver archivo contributors.txt
:Fecha:
07/12/2015
-:Versión:
- 0.2.8
+:Ultima Versión:
+ 0.3.0
Descripción
@@ -27,7 +27,7 @@ Pequeño sistema para administrar CFDIs; facturas electrónicas de México. Entr
Requerimientos
--------------
-* Python 3.4
+* Python 3.2+
* Tk si usas Linux. Si usas Windows, ya lo integra Python.
* Firefox para la automatización de la descarga del SAT.
* Selenium para la automatización de la descarga del SAT.
@@ -37,23 +37,16 @@ Requerimientos
Instalación
-----------
-Si tienes instalado correctamente Python 3.4, puedes instalar con Pip.
+Si tienes instalado correctamente Python 3.2+, puedes instalar con Pip.
GNU & LInux
###########
::
- sudo pip install selenium pygubu
- python admincfdi.py
+ sudo python setup.py install
+ admin-cfdi
-ArchLinux
-_________
-
-::
-
- sudo pip install selenium pygubu
- python admincfdi.py
Linux Mint
__________
@@ -61,8 +54,8 @@ __________
::
sudo apt-get install python3-pip python3-tk
- sudo pip3 install selenium pygubu
- python3 admincfdi.py
+ sudo python setup.py install
+ admin-cfdi
Windows
@@ -72,7 +65,7 @@ Si usas Windows, asegúrate de abrir el script con el ejecutable pythonw.exe loc
::
- pip install selenium pygubu
+ python setup.py install
Ligas
diff --git a/admincfdi.py b/admin-cfdi
old mode 100644
new mode 100755
similarity index 78%
rename from admincfdi.py
rename to admin-cfdi
index b2d7f1a..8552d75
--- a/admincfdi.py
+++ b/admin-cfdi
@@ -14,12 +14,9 @@
import tkinter as tk
import pygubu
from selenium import webdriver
-from pyutil import Util
-from pyutil import Mail
-from pyutil import LibO
-from pyutil import APP_LIBO
-from pyutil import CFDIPDF
-from values import Global
+from admincfdi.pyutil import Util, Mail, LibO, CFDIPDF, DescargaSAT
+from admincfdi.values import Global
+from admincfdi.pyutil import LIBO
class Application(pygubu.TkApplication):
@@ -139,14 +136,13 @@ def _create_ui(self):
combo = self._get_object('combo_end_second')
self.util.combo_values(combo, minutes, 59)
- if not APP_LIBO:
- self._config('radio_ods', {'state': 'disabled'})
- self._config('radio_json', {'state': 'disabled'})
- self._config('button_select_template_json', {'state': 'disabled'})
-
self._set('mail_port', 993)
self._get_object('check_ssl').invoke()
self._focus_set('text_rfc')
+
+ if not LIBO:
+ self._config('radio_ods', {'state': 'disabled'})
+ self.pb = self._get_object('progressbar')
return
def _center_window(self, root):
@@ -283,10 +279,10 @@ def radio_template_type(self, event):
radio = 'radio_ods'
button = 'button_select_template_ods'
self._config(button, {'state': 'normal'})
- self._config('button_select_template_json', {'state': 'normal'})
+ self._config('button_select_template_csv', {'state': 'normal'})
if opt == 1:
- radio = 'radio_json'
- button = 'button_select_template_json'
+ radio = 'radio_csv'
+ button = 'button_select_template_csv'
self._config(radio, {'bg': self.g.COLORS['DEFAULT']})
self._config(button, {'state': 'disabled'})
return
@@ -389,235 +385,93 @@ def combo_month_click(self, event):
self.util.combo_values(combo, days, 0)
return
+ def msg_user(self, msg):
+ self._set('msg_user', msg, True)
+
+ def progress(self, value, maximum):
+ pb = self._get_object('progressbar')
+ pb['value'] = value
+ pb['maximum'] = maximum
+ self.parent.update_idletasks()
+
def button_download_sat_click(self):
ok, data = self._validate_download_sat()
if not ok:
return
- self._download_sat(data)
- return
-
- def _download_sat(self, data):
- self._set('msg_user', 'Abriendo Firefox...', True)
- page_query = self.g.SAT['page_receptor']
- if data['type_invoice'] == 1:
- page_query = self.g.SAT['page_emisor']
- # To prevent download dialog
- profile = webdriver.FirefoxProfile()
- profile.set_preference(
- 'browser.download.folderList', 2)
- profile.set_preference(
- 'browser.download.manager.showWhenStarting', False)
- profile.set_preference(
- 'browser.helperApps.alwaysAsk.force', False)
- profile.set_preference(
- 'browser.helperApps.neverAsk.saveToDisk',
- 'text/xml, application/octet-stream, application/xml')
- profile.set_preference(
- 'browser.download.dir', data['user_sat']['target_sat'])
- # mrE - desactivar telemetry
- profile.set_preference(
- 'toolkit.telemetry.prompted', 2)
- profile.set_preference(
- 'toolkit.telemetry.rejected', True)
- profile.set_preference(
- 'toolkit.telemetry.enabled', False)
- profile.set_preference(
- 'datareporting.healthreport.service.enabled', False)
- profile.set_preference(
- 'datareporting.healthreport.uploadEnabled', False)
- profile.set_preference(
- 'datareporting.healthreport.service.firstRun', False)
- profile.set_preference(
- 'datareporting.healthreport.logging.consoleEnabled', False)
- profile.set_preference(
- 'datareporting.policy.dataSubmissionEnabled', False)
- profile.set_preference(
- 'datareporting.policy.dataSubmissionPolicyResponseType', 'accepted-info-bar-dismissed')
- #profile.set_preference(
- # 'datareporting.policy.dataSubmissionPolicyAccepted'; False) # este me marca error, why?
- #oculta la gran flecha animada al descargar
- profile.set_preference(
- 'browser.download.animateNotifications', False)
+ descarga = DescargaSAT(
+ status_callback=self.msg_user, download_callback=self.progress)
+ profile = descarga.get_firefox_profile(data['carpeta_destino'])
try:
- pb = self._get_object('progressbar')
- browser = webdriver.Firefox(profile)
- self._set('msg_user', 'Conectando...', True)
- browser.get(self.g.SAT['page_init'])
- txt = browser.find_element_by_name(self.g.SAT['user'])
- txt.send_keys(data['user_sat']['user_sat'])
- txt = browser.find_element_by_name(self.g.SAT['password'])
- txt.send_keys(data['user_sat']['password'])
- txt.submit()
- self.util.sleep(3)
- self._set('msg_user', 'Conectado...', True)
- browser.get(page_query)
- self.util.sleep(5)
- self._set('msg_user', 'Buscando...', True)
- if data['type_search'] == 1:
- txt = browser.find_element_by_id(self.g.SAT['uuid'])
- txt.click()
- txt.send_keys(data['search_uuid'])
- else:
- # Descargar por fecha
- opt = browser.find_element_by_id(self.g.SAT['date'])
- opt.click()
- self.util.sleep()
- if data['search_rfc']:
- if data['type_search'] == 1:
- txt = browser.find_element_by_id(self.g.SAT['receptor'])
- else:
- txt = browser.find_element_by_id(self.g.SAT['emisor'])
- txt.send_keys(data['search_rfc'])
- # Emitidas
- if data['type_invoice'] == 1:
- year = int(data['search_year'])
- month = int(data['search_month'])
- dates = self.util.get_dates(year, month)
- txt = browser.find_element_by_id(self.g.SAT['date_from'])
- arg = "document.getElementsByName('{}')[0]." \
- "removeAttribute('disabled');".format(
- self.g.SAT['date_from_name'])
- browser.execute_script(arg)
- txt.send_keys(dates[0])
- txt = browser.find_element_by_id(self.g.SAT['date_to'])
- arg = "document.getElementsByName('{}')[0]." \
- "removeAttribute('disabled');".format(
- self.g.SAT['date_to_name'])
- browser.execute_script(arg)
- txt.send_keys(dates[1])
- # Recibidas
- else:
- arg = "document.getElementById('{}').value={};".format(
- self.g.SAT['year'], data['search_year'])
- browser.execute_script(arg)
- arg = "document.getElementById('{}').value={};".format(
- self.g.SAT['month'], data['search_month'])
- browser.execute_script(arg)
-
- if data['search_day'] != '00':
- arg = "document.getElementById('{}').value='{}';".format(
- self.g.SAT['day'], data['search_day'])
- browser.execute_script(arg)
- self.util.sleep()
-
- #Establece hora de inicio y fin
- values = (
- 'start_hour', 'start_minute', 'start_second',
- 'end_hour', 'end_minute', 'end_second'
- )
- for v in values:
- arg = "document.getElementById('{}').value={};".format(
- self.g.SAT[v], data[v])
- browser.execute_script(arg)
- self.util.sleep()
-
- browser.find_element_by_id(self.g.SAT['submit']).click()
- sec = 3
- if data['type_invoice'] != 1 and data['search_day'] == '00':
- sec = 15
- self.util.sleep(sec)
- # Bug del SAT
- if data['type_invoice'] != 1 and data['search_day'] != '00':
- arg = "document.getElementById('{}').value='{}';".format(
- self.g.SAT['day'], data['search_day'])
- browser.execute_script(arg)
- self.util.sleep(2)
- browser.find_element_by_id(self.g.SAT['submit']).click()
- self.util.sleep(sec)
- elif data['type_invoice'] == 2 and data['sat_month']:
- return self._download_sat_month(data, browser)
-
- try:
- found = True
- content = browser.find_elements_by_class_name(
- self.g.SAT['subtitle'])
- for c in content:
- if self.g.SAT['found'] in c.get_attribute('innerHTML') \
- and c.is_displayed():
- found = False
- break
- except Exception as e:
- print (str(e))
-
- if found:
- docs = browser.find_elements_by_name(self.g.SAT['download'])
- t = len(docs)
- pb['maximum'] = t
- pb.start()
- for i, v in enumerate(docs):
- msg = 'Factura {} de {}'.format(i+1, t)
- pb['value'] = i + 1
- self._set('msg_user', msg, True)
- download = self.g.SAT['page_cfdi'].format(
- v.get_attribute('onclick').split("'")[1])
- browser.get(download)
- pb['value'] = 0
- pb.stop()
- self.util.sleep()
- else:
- self._set('msg_user', 'Sin facturas...', True)
+ descarga.connect(profile, rfc=data['rfc'], ciec=data['ciec'])
+ docs = descarga.search(
+ facturas_emitidas=data['facturas_emitidas'],
+ uuid=data['uuid'],
+ rfc_emisor=data['rfc_emisor'],
+ año=data['año'],
+ mes=data['mes'],
+ día=data['día'],
+ hora_inicial=data['start_hour'],
+ minuto_inicial=data['start_minute'],
+ segundo_inicial=data['start_second'],
+ hora_final=data['end_hour'],
+ minuto_final=data['end_minute'],
+ segundo_final=data['end_second'],
+ mes_completo_por_día=data['mes_completo_por_día'])
+ descarga.download(docs)
except Exception as e:
- print (e)
+ self.g.LOG.error("Descarga SAT: \n" + str(e))
finally:
- pb['value'] = 0
- pb.stop()
- try:
- self._set('msg_user', 'Desconectando...', True)
- link = browser.find_element_by_partial_link_text('Cerrar Sesi')
- link.click()
- except:
- pass
- finally:
- browser.close()
- self._set('msg_user', 'Desconectado...')
- return
-
- def _download_sat_month(self, data, browser):
- year = int(data['search_year'])
- month = int(data['search_month'])
- days_month = self.util.get_days(year, month) + 1
- days = ['%02d' % x for x in range(1, days_month)]
- for d in days:
- combo = browser.find_element_by_id(self.g.SAT['day'])
- sb = combo.get_attribute('sb')
- combo = browser.find_element_by_id('sbToggle_{}'.format(sb))
- combo.click()
- self.util.sleep(2)
- if data['search_month'] == d:
- links = browser.find_elements_by_link_text(d)
- for l in links:
- p = l.find_element_by_xpath(
- '..').find_element_by_xpath('..')
- sb2 = p.get_attribute('id')
- if sb in sb2:
- link = l
- break
- else:
- link = browser.find_element_by_link_text(d)
- link.click()
- self.util.sleep(2)
- browser.find_element_by_id(self.g.SAT['submit']).click()
- self.util.sleep(3)
- docs = browser.find_elements_by_name(self.g.SAT['download'])
- if docs:
- t = len(docs)
- pb = self._get_object('progressbar')
- pb['maximum'] = t
- pb.start()
- for i, v in enumerate(docs):
- msg = 'Factura {} de {}'.format(i+1, t)
- pb['value'] = i + 1
- self._set('msg_user', msg, True)
- download = self.g.SAT['page_cfdi'].format(
- v.get_attribute('onclick').split("'")[1])
- browser.get(download)
- self.util.sleep()
- pb['value'] = 0
- pb.stop()
- self.util.sleep()
+ descarga.disconnect()
return
def _validate_download_sat(self):
+ '''Valida requisitos y crea datos para descarga
+
+ Las validaciones son:
+
+ - Al menos una tríada RFC, CIEC y carpeta
+ destino ha sido registrada
+ - Una tríada RFC, CIEC y carpeta destino
+ está seleccionada
+ - La UUID no es nula y tiene 36 caracteres,
+ si se seleccionó *Buscar por folio
+ fiscal (UUID)*
+ - El RFC del emisor tiene 12 o 13
+ caracteres, si se proporciona
+
+ Si falta alguna de estas condiciones,
+ se abre un diálogo con un texto informativo
+ y un botón 'OK' en el ambiente gráfico
+ y se regresa (False, {})
+
+ Si las validaciones pasan, se construye un
+ diccionario ``data`` con estas llaves y valores
+ requeridos por la API de descarga:
+
+ - user_sat: un diccionario con el
+ RFC, la CIEC y la carpeta destino
+ que se seleccionaron, con las llaves
+ ``user_sat``, ``password`` y ``target_sat``.
+ - facturas_emitidas: True si la selección hecha en
+ *Tipo de consulta* es 1 facturas emitidas, False
+ si la selección es 2 facturas recibidas
+ - search_uuid: el valor llenado para *UUID*,
+ es cadena vacía por omisión
+ - search_rfc: el valor llenado en *RFC Emisor*,
+ es cadena vacía por omisión
+ - search_year: el valor seleccionado en *Año*
+ como cadena, es el año en curso por omisión
+ - search_month: el valor seleccionado en *Mes*
+ como cadena, es el mes en curso por omisión
+ - search_day: el valor seleccionado en *Día*
+ como cadena, es '00' por omisión o si sat_month
+ es verdadero.
+ - sat_month: Representa la caja a la izquierda de
+ *Descargar mes completo por día*.
+
+ La función regresa (True, data)
+ '''
+
data = {}
if not self.users_sat:
msg = 'Agrega un RFC y contraseña a consultar'
@@ -651,20 +505,19 @@ def _validate_download_sat(self):
self.util.msgbox(msg)
return False, data
sat_month = self._get('sat_month')
- if sat_month:
- search_day = '00'
- else:
- search_day = self._get('search_day')
+ search_day = self._get('search_day')
+ user = self.users_sat[current_user]
data = {
- 'user_sat': self.users_sat[current_user],
- 'type_invoice': self._get('type_invoice'),
- 'type_search': opt,
- 'search_uuid': uuid,
- 'search_rfc': rfc,
- 'search_year': self._get('search_year'),
- 'search_month': self._get('search_month'),
- 'search_day': search_day,
- 'sat_month': sat_month,
+ 'facturas_emitidas': self._get('type_invoice') == 1,
+ 'rfc': user['user_sat'],
+ 'ciec': user['password'],
+ 'carpeta_destino': user['target_sat'],
+ 'uuid': uuid,
+ 'rfc_emisor': rfc,
+ 'año': self._get('search_year'),
+ 'mes': self._get('search_month'),
+ 'día': search_day,
+ 'mes_completo_por_día': sat_month,
'start_hour': self._get('start_hour'),
'start_minute': self._get('start_minute'),
'start_second': self._get('start_second'),
@@ -1074,20 +927,20 @@ def button_select_folder_source_pdf_click(self):
def button_select_folder_target_pdf_click(self):
folder = self.util.get_folder(self.parent)
if folder:
- self._set('target_pdf', folder)
+ self._set('pdf_target', folder)
return
def button_select_template_ods_click(self):
file_name = self.util.get_file(self.parent, ext=self.g.EXT_ODS)
if file_name:
self._set('template_ods', file_name)
- self._set('template_json')
+ self._set('template_csv')
return
- def button_select_template_json_click(self):
- file_name = self.util.get_file(self.parent, ext=self.g.EXT_JSON)
+ def button_select_template_csv_click(self):
+ file_name = self.util.get_file(self.parent, ext=self.g.EXT_CSV)
if file_name:
- self._set('template_json', file_name)
+ self._set('template_csv', file_name)
self._set('template_ods')
return
@@ -1104,7 +957,7 @@ def button_save_pdf_user_click(self):
'pdf_source',
'pdf_target',
'template_ods',
- 'template_json'
+ 'template_csv'
)
for v in var:
self._set(v)
@@ -1162,13 +1015,13 @@ def _validate_pdf_user(self):
self.util.msgbox(msg)
return False, data
template_ods = self.util.path_config(template_ods)
- template_json = self._get('template_json')
- if template_json:
- if not self.util.validate_dir(template_json):
- msg = 'No se encontró la plantilla JSON'
+ template_csv = self._get('template_csv')
+ if template_csv:
+ if not self.util.validate_dir(template_csv):
+ msg = 'No se encontró la plantilla CSV'
self.util.msgbox(msg)
return False, data
- template_json = self.util.path_config(template_json)
+ template_csv = self.util.path_config(template_csv)
template_type = self._get('template_type')
if template_type == 1:
@@ -1177,8 +1030,8 @@ def _validate_pdf_user(self):
self.util.msgbox(msg)
return False, data
elif template_type == 2:
- if not template_json:
- msg = 'Selecciona una plantilla JSON'
+ if not template_csv:
+ msg = 'Selecciona una plantilla CSV'
self.util.msgbox(msg)
return False, data
@@ -1186,7 +1039,7 @@ def _validate_pdf_user(self):
data['pdf_source'] = self.util.path_config(pdf_source)
data['pdf_target'] = self.util.path_config(pdf_target)
data['template_ods'] = template_ods
- data['template_json'] = template_json
+ data['template_csv'] = template_csv
#~ data['pdf_print'] = self._get('pdf_print')
return True, data
@@ -1199,7 +1052,7 @@ def button_delete_pdf_user_click(self):
'pdf_source',
'pdf_target',
'template_ods',
- 'template_json',
+ 'template_csv',
)
for v in var:
self._set(v, self.users_pdf[sel][v])
@@ -1219,38 +1072,53 @@ def button_generate_pdf_click(self):
if not ok:
return
- files = data['files']
- del data['files']
- total = len(files)
+ if data['pdf_user']['template_csv']:
+ self._csv_to_pdf(data)
+ return
- libo = LibO()
- if libo.SM is None:
- del libo
- msg = 'No fue posible iniciar LibreOffice, asegurate de que ' \
- 'este correctamente instalado e intentalo de nuevo'
- self.util.msgbox(msg)
- return False, data
+ self._ods_to_pdf(data)
+ return
- pb = self._get_object('progressbar')
- pb['maximum'] = total
- pb.start()
+ def _ods_to_pdf(self, data):
+ files = data['files']
+ total = len(files)
+ self.pb['maximum'] = total
+ self.pb.start()
j = 0
for i, f in enumerate(files):
- pb['value'] = i + 1
+ self.pb['value'] = i + 1
msg = 'Archivo {} de {}'.format(i + 1, total)
self._set('msg_user', msg)
self.parent.update_idletasks()
- if self._make_pdf(f, libo, data['pdf_user']):
+ if self._make_pdf(f, data['libo'], data['pdf_user']):
j += 1
- del libo
- pb['value'] = 0
- pb.stop()
+ del data['libo']
+ self.pb['value'] = 0
+ self.pb.stop()
msg = 'Archivos XML encontrados: {}\n'
msg += 'Archivos PDF generados: {}'
msg = msg.format(total, j)
self.util.msgbox(msg)
return
+ def _csv_to_pdf(self, data):
+ files = data['files']
+ user = data['pdf_user']
+ total = len(files)
+ self.pb['maximum'] = total
+ self.pb.start()
+ for i, f in enumerate(files):
+ args = (self.g.SCRIPTS['CFDI2PDF'], '-a', f, '-o',
+ user['pdf_source'], '-d', user['pdf_target'], '-p', user['template_csv'])
+ self.util.call(args)
+ self.pb['value'] = i + 1
+ msg = 'Archivo {} de {}'.format(i + 1, total)
+ self._set('msg_user', msg)
+ self.parent.update_idletasks()
+ self.pb['value'] = 0
+ self.pb.stop()
+ return
+
def _make_pdf(self, path, libo, data):
xml = self.util.parse(path)
if xml is None:
@@ -1258,8 +1126,8 @@ def _make_pdf(self, path, libo, data):
source = data['pdf_source']
target = data['pdf_target']
template = data['template_ods']
- if data['template_json']:
- template = data['template_json']
+ if data['template_csv']:
+ template = data['template_csv']
info = self.util.get_path_info(path)
path_pdf = self.util.join(target, info[0][len(source)+1:])
self.util.makedirs(path_pdf)
@@ -1278,36 +1146,46 @@ def _make_pdf(self, path, libo, data):
return False
def _validate_pdf(self):
+ data = {}
pdf_user = self._get('current_user_pdf')
if not pdf_user:
msg = 'Selecciona un usuario primero'
self.util.msgbox(msg)
return False, {}
- data = self.users_pdf[pdf_user]
- if not self.util.validate_dir(data['pdf_source'], 'r'):
+ pdf_user = self.users_pdf[pdf_user]
+ if not self.util.validate_dir(pdf_user['pdf_source'], 'r'):
msg = 'No tienes derechos de lectura en el directorio origen'
self.util.msgbox(msg)
return False, {}
- files = self.util.get_files(data['pdf_source'])
+ files = self.util.get_files(pdf_user['pdf_source'])
if not files:
msg = 'No se encontraron archivos XML en el directorio origen'
self.util.msgbox(msg)
return False, {}
- if not self.util.validate_dir(data['pdf_target'], 'w'):
+ if not self.util.validate_dir(pdf_user['pdf_target'], 'w'):
msg = 'No tienes derechos de escritura en el directorio destino'
self.util.msgbox(msg)
return False, {}
- template_ods = data['template_ods']
- if data['template_ods']:
- if not self.util.exists(data['template_ods']):
+ data['pdf_user'] = pdf_user
+ data['files'] = files
+ data['libo'] = None
+ if pdf_user['template_csv']:
+ msg = 'Se van a generar {} PDFs\n\n¿Estas seguro de continuar?'
+ if not self.util.question(msg.format(len(files))):
+ return False, {}
+ return True, data
+
+ #~ template_ods = pdf_user['template_ods']
+ if pdf_user['template_ods']:
+ if not self.util.exists(pdf_user['template_ods']):
msg = 'No se encontró una plantilla en la ruta establecida'
self.util.msgbox(msg)
return False, {}
- ext = self.util.get_path_info(data['template_ods'], 3)
+ ext = self.util.get_path_info(pdf_user['template_ods'], 3)
if ext != self.g.EXT_ODS:
msg = 'La plantilla no es un archivo ODS de Calc'
self.util.msgbox(msg)
@@ -1318,25 +1196,12 @@ def _validate_pdf(self):
msg = 'No fue posible iniciar LibreOffice, asegurate de que ' \
'este correctamente instalado e intentalo de nuevo'
self.util.msgbox(msg)
- return False, data
- doc = libo.doc_open(data['template_ods'])
- if doc is None:
- del libo
- msg = 'No fue posible abrir la plantilla, asegurate de que ' \
- 'la ruta sea correcta'
- self.util.msgbox(msg)
- return False, data
- doc.dispose()
- del libo
- else: # ToDO CSV
- return False, {}
+ return False, {}
msg = 'Se van a generar {} PDFs\n\n¿Estas seguro de continuar?'
if not self.util.question(msg.format(len(files))):
return False, {}
-
- data['pdf_user'] = data
- data['files'] = files
+ data['libo'] = libo
return True, data
def button_select_folder_report_source_click(self):
diff --git a/admin-cfdi.pyw b/admin-cfdi.pyw
new file mode 100755
index 0000000..e948acf
--- /dev/null
+++ b/admin-cfdi.pyw
@@ -0,0 +1,6 @@
+import sys
+import os.path
+
+
+ac_path = os.path.join(sys.path[0], "admin-cfdi")
+exec(open(ac_path, encoding='utf-8').read())
diff --git a/admincfdi.log b/admincfdi.log
deleted file mode 100644
index e69de29..0000000
diff --git a/__init__.py b/admincfdi/__init__.py
similarity index 100%
rename from __init__.py
rename to admincfdi/__init__.py
diff --git a/bin/cadena3.2.xslt b/admincfdi/bin/cadena3.2.xslt
similarity index 100%
rename from bin/cadena3.2.xslt
rename to admincfdi/bin/cadena3.2.xslt
diff --git a/bin/cfdi_3.2.xsd b/admincfdi/bin/cfdi_3.2.xsd
similarity index 100%
rename from bin/cfdi_3.2.xsd
rename to admincfdi/bin/cfdi_3.2.xsd
diff --git a/bin/cfdi_3.2.xslt b/admincfdi/bin/cfdi_3.2.xslt
similarity index 84%
rename from bin/cfdi_3.2.xslt
rename to admincfdi/bin/cfdi_3.2.xslt
index 5c6a0d4..89c26d1 100644
--- a/bin/cfdi_3.2.xslt
+++ b/admincfdi/bin/cfdi_3.2.xslt
@@ -406,6 +406,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |||
diff --git a/bin/get_certificado.xslt b/admincfdi/bin/get_certificado.xslt
similarity index 93%
rename from bin/get_certificado.xslt
rename to admincfdi/bin/get_certificado.xslt
index e003d1b..2eb2f8a 100644
--- a/bin/get_certificado.xslt
+++ b/admincfdi/bin/get_certificado.xslt
@@ -51,6 +51,10 @@
+
+
+
+
diff --git a/bin/get_sello.xslt b/admincfdi/bin/get_sello.xslt
similarity index 82%
rename from bin/get_sello.xslt
rename to admincfdi/bin/get_sello.xslt
index 65d0165..af45a03 100644
--- a/bin/get_sello.xslt
+++ b/admincfdi/bin/get_sello.xslt
@@ -4,7 +4,7 @@
-
+
diff --git a/bin/get_sello_sat.xslt b/admincfdi/bin/get_sello_sat.xslt
similarity index 100%
rename from bin/get_sello_sat.xslt
rename to admincfdi/bin/get_sello_sat.xslt
diff --git a/bin/iconv.dll b/admincfdi/bin/iconv.dll
similarity index 100%
rename from bin/iconv.dll
rename to admincfdi/bin/iconv.dll
diff --git a/bin/libeay32.dll b/admincfdi/bin/libeay32.dll
similarity index 100%
rename from bin/libeay32.dll
rename to admincfdi/bin/libeay32.dll
diff --git a/bin/libexslt.dll b/admincfdi/bin/libexslt.dll
similarity index 100%
rename from bin/libexslt.dll
rename to admincfdi/bin/libexslt.dll
diff --git a/bin/libxml2.dll b/admincfdi/bin/libxml2.dll
similarity index 100%
rename from bin/libxml2.dll
rename to admincfdi/bin/libxml2.dll
diff --git a/bin/libxslt.dll b/admincfdi/bin/libxslt.dll
similarity index 100%
rename from bin/libxslt.dll
rename to admincfdi/bin/libxslt.dll
diff --git a/bin/openssl.exe b/admincfdi/bin/openssl.exe
similarity index 100%
rename from bin/openssl.exe
rename to admincfdi/bin/openssl.exe
diff --git a/bin/ssleay32.dll b/admincfdi/bin/ssleay32.dll
similarity index 100%
rename from bin/ssleay32.dll
rename to admincfdi/bin/ssleay32.dll
diff --git a/bin/timbre.xslt b/admincfdi/bin/timbre.xslt
similarity index 100%
rename from bin/timbre.xslt
rename to admincfdi/bin/timbre.xslt
diff --git a/bin/timbre_1.0.xslt b/admincfdi/bin/timbre_1.0.xslt
similarity index 100%
rename from bin/timbre_1.0.xslt
rename to admincfdi/bin/timbre_1.0.xslt
diff --git a/bin/xsltproc.exe b/admincfdi/bin/xsltproc.exe
similarity index 100%
rename from bin/xsltproc.exe
rename to admincfdi/bin/xsltproc.exe
diff --git a/bin/zlib1.dll b/admincfdi/bin/zlib1.dll
similarity index 100%
rename from bin/zlib1.dll
rename to admincfdi/bin/zlib1.dll
diff --git a/cer_pac/00001000000103834451.cer b/admincfdi/cer_pac/00001000000103834451.cer
similarity index 100%
rename from cer_pac/00001000000103834451.cer
rename to admincfdi/cer_pac/00001000000103834451.cer
diff --git a/cer_pac/00001000000104731997.cer b/admincfdi/cer_pac/00001000000104731997.cer
similarity index 100%
rename from cer_pac/00001000000104731997.cer
rename to admincfdi/cer_pac/00001000000104731997.cer
diff --git a/cer_pac/00001000000104750010.cer b/admincfdi/cer_pac/00001000000104750010.cer
similarity index 100%
rename from cer_pac/00001000000104750010.cer
rename to admincfdi/cer_pac/00001000000104750010.cer
diff --git a/cer_pac/00001000000104871381.cer b/admincfdi/cer_pac/00001000000104871381.cer
similarity index 100%
rename from cer_pac/00001000000104871381.cer
rename to admincfdi/cer_pac/00001000000104871381.cer
diff --git a/cer_pac/00001000000104888042.cer b/admincfdi/cer_pac/00001000000104888042.cer
similarity index 100%
rename from cer_pac/00001000000104888042.cer
rename to admincfdi/cer_pac/00001000000104888042.cer
diff --git a/cer_pac/00001000000200005634.cer b/admincfdi/cer_pac/00001000000200005634.cer
similarity index 100%
rename from cer_pac/00001000000200005634.cer
rename to admincfdi/cer_pac/00001000000200005634.cer
diff --git a/cer_pac/00001000000200011997.cer b/admincfdi/cer_pac/00001000000200011997.cer
similarity index 100%
rename from cer_pac/00001000000200011997.cer
rename to admincfdi/cer_pac/00001000000200011997.cer
diff --git a/cer_pac/00001000000200365214.cer b/admincfdi/cer_pac/00001000000200365214.cer
similarity index 100%
rename from cer_pac/00001000000200365214.cer
rename to admincfdi/cer_pac/00001000000200365214.cer
diff --git a/cer_pac/00001000000200795916.cer b/admincfdi/cer_pac/00001000000200795916.cer
similarity index 100%
rename from cer_pac/00001000000200795916.cer
rename to admincfdi/cer_pac/00001000000200795916.cer
diff --git a/cer_pac/00001000000201345662.cer b/admincfdi/cer_pac/00001000000201345662.cer
similarity index 100%
rename from cer_pac/00001000000201345662.cer
rename to admincfdi/cer_pac/00001000000201345662.cer
diff --git a/cer_pac/00001000000201345708.cer b/admincfdi/cer_pac/00001000000201345708.cer
similarity index 100%
rename from cer_pac/00001000000201345708.cer
rename to admincfdi/cer_pac/00001000000201345708.cer
diff --git a/cer_pac/00001000000201395217.cer b/admincfdi/cer_pac/00001000000201395217.cer
similarity index 100%
rename from cer_pac/00001000000201395217.cer
rename to admincfdi/cer_pac/00001000000201395217.cer
diff --git a/cer_pac/00001000000201455572.cer b/admincfdi/cer_pac/00001000000201455572.cer
similarity index 100%
rename from cer_pac/00001000000201455572.cer
rename to admincfdi/cer_pac/00001000000201455572.cer
diff --git a/cer_pac/00001000000201614141.cer b/admincfdi/cer_pac/00001000000201614141.cer
similarity index 100%
rename from cer_pac/00001000000201614141.cer
rename to admincfdi/cer_pac/00001000000201614141.cer
diff --git a/cer_pac/00001000000201629292.cer b/admincfdi/cer_pac/00001000000201629292.cer
similarity index 100%
rename from cer_pac/00001000000201629292.cer
rename to admincfdi/cer_pac/00001000000201629292.cer
diff --git a/cer_pac/00001000000201748120.cer b/admincfdi/cer_pac/00001000000201748120.cer
similarity index 100%
rename from cer_pac/00001000000201748120.cer
rename to admincfdi/cer_pac/00001000000201748120.cer
diff --git a/cer_pac/00001000000202241710.cer b/admincfdi/cer_pac/00001000000202241710.cer
similarity index 100%
rename from cer_pac/00001000000202241710.cer
rename to admincfdi/cer_pac/00001000000202241710.cer
diff --git a/cer_pac/00001000000202453260.cer b/admincfdi/cer_pac/00001000000202453260.cer
similarity index 100%
rename from cer_pac/00001000000202453260.cer
rename to admincfdi/cer_pac/00001000000202453260.cer
diff --git a/cer_pac/00001000000202638162.cer b/admincfdi/cer_pac/00001000000202638162.cer
similarity index 100%
rename from cer_pac/00001000000202638162.cer
rename to admincfdi/cer_pac/00001000000202638162.cer
diff --git a/cer_pac/00001000000202639096.cer b/admincfdi/cer_pac/00001000000202639096.cer
similarity index 100%
rename from cer_pac/00001000000202639096.cer
rename to admincfdi/cer_pac/00001000000202639096.cer
diff --git a/cer_pac/00001000000202639521.cer b/admincfdi/cer_pac/00001000000202639521.cer
similarity index 100%
rename from cer_pac/00001000000202639521.cer
rename to admincfdi/cer_pac/00001000000202639521.cer
diff --git a/cer_pac/00001000000202693892.cer b/admincfdi/cer_pac/00001000000202693892.cer
similarity index 100%
rename from cer_pac/00001000000202693892.cer
rename to admincfdi/cer_pac/00001000000202693892.cer
diff --git a/cer_pac/00001000000202695775.cer b/admincfdi/cer_pac/00001000000202695775.cer
similarity index 100%
rename from cer_pac/00001000000202695775.cer
rename to admincfdi/cer_pac/00001000000202695775.cer
diff --git a/cer_pac/00001000000202700691.cer b/admincfdi/cer_pac/00001000000202700691.cer
similarity index 100%
rename from cer_pac/00001000000202700691.cer
rename to admincfdi/cer_pac/00001000000202700691.cer
diff --git a/cer_pac/00001000000202771790.cer b/admincfdi/cer_pac/00001000000202771790.cer
similarity index 100%
rename from cer_pac/00001000000202771790.cer
rename to admincfdi/cer_pac/00001000000202771790.cer
diff --git a/cer_pac/00001000000202772539.cer b/admincfdi/cer_pac/00001000000202772539.cer
similarity index 100%
rename from cer_pac/00001000000202772539.cer
rename to admincfdi/cer_pac/00001000000202772539.cer
diff --git a/cer_pac/00001000000202809550.cer b/admincfdi/cer_pac/00001000000202809550.cer
similarity index 100%
rename from cer_pac/00001000000202809550.cer
rename to admincfdi/cer_pac/00001000000202809550.cer
diff --git a/cer_pac/00001000000202864285.cer b/admincfdi/cer_pac/00001000000202864285.cer
similarity index 100%
rename from cer_pac/00001000000202864285.cer
rename to admincfdi/cer_pac/00001000000202864285.cer
diff --git a/cer_pac/00001000000202864530.cer b/admincfdi/cer_pac/00001000000202864530.cer
similarity index 100%
rename from cer_pac/00001000000202864530.cer
rename to admincfdi/cer_pac/00001000000202864530.cer
diff --git a/cer_pac/00001000000202864883.cer b/admincfdi/cer_pac/00001000000202864883.cer
similarity index 100%
rename from cer_pac/00001000000202864883.cer
rename to admincfdi/cer_pac/00001000000202864883.cer
diff --git a/cer_pac/00001000000202865018.cer b/admincfdi/cer_pac/00001000000202865018.cer
similarity index 100%
rename from cer_pac/00001000000202865018.cer
rename to admincfdi/cer_pac/00001000000202865018.cer
diff --git a/cer_pac/00001000000202905407.cer b/admincfdi/cer_pac/00001000000202905407.cer
similarity index 100%
rename from cer_pac/00001000000202905407.cer
rename to admincfdi/cer_pac/00001000000202905407.cer
diff --git a/cer_pac/00001000000203015571.cer b/admincfdi/cer_pac/00001000000203015571.cer
similarity index 100%
rename from cer_pac/00001000000203015571.cer
rename to admincfdi/cer_pac/00001000000203015571.cer
diff --git a/cer_pac/00001000000203051706.cer b/admincfdi/cer_pac/00001000000203051706.cer
similarity index 100%
rename from cer_pac/00001000000203051706.cer
rename to admincfdi/cer_pac/00001000000203051706.cer
diff --git a/cer_pac/00001000000203082087.cer b/admincfdi/cer_pac/00001000000203082087.cer
similarity index 100%
rename from cer_pac/00001000000203082087.cer
rename to admincfdi/cer_pac/00001000000203082087.cer
diff --git a/cer_pac/00001000000203092957.cer b/admincfdi/cer_pac/00001000000203092957.cer
similarity index 100%
rename from cer_pac/00001000000203092957.cer
rename to admincfdi/cer_pac/00001000000203092957.cer
diff --git a/cer_pac/00001000000203093174.cer b/admincfdi/cer_pac/00001000000203093174.cer
similarity index 100%
rename from cer_pac/00001000000203093174.cer
rename to admincfdi/cer_pac/00001000000203093174.cer
diff --git a/cer_pac/00001000000203159220.cer b/admincfdi/cer_pac/00001000000203159220.cer
similarity index 100%
rename from cer_pac/00001000000203159220.cer
rename to admincfdi/cer_pac/00001000000203159220.cer
diff --git a/cer_pac/00001000000203159375.cer b/admincfdi/cer_pac/00001000000203159375.cer
similarity index 100%
rename from cer_pac/00001000000203159375.cer
rename to admincfdi/cer_pac/00001000000203159375.cer
diff --git a/cer_pac/00001000000203191015.cer b/admincfdi/cer_pac/00001000000203191015.cer
similarity index 100%
rename from cer_pac/00001000000203191015.cer
rename to admincfdi/cer_pac/00001000000203191015.cer
diff --git a/cer_pac/00001000000203220518.cer b/admincfdi/cer_pac/00001000000203220518.cer
similarity index 100%
rename from cer_pac/00001000000203220518.cer
rename to admincfdi/cer_pac/00001000000203220518.cer
diff --git a/cer_pac/00001000000203220546.cer b/admincfdi/cer_pac/00001000000203220546.cer
similarity index 100%
rename from cer_pac/00001000000203220546.cer
rename to admincfdi/cer_pac/00001000000203220546.cer
diff --git a/cer_pac/00001000000203253077.cer b/admincfdi/cer_pac/00001000000203253077.cer
similarity index 100%
rename from cer_pac/00001000000203253077.cer
rename to admincfdi/cer_pac/00001000000203253077.cer
diff --git a/cer_pac/00001000000203285726.cer b/admincfdi/cer_pac/00001000000203285726.cer
similarity index 100%
rename from cer_pac/00001000000203285726.cer
rename to admincfdi/cer_pac/00001000000203285726.cer
diff --git a/cer_pac/00001000000203285735.cer b/admincfdi/cer_pac/00001000000203285735.cer
similarity index 100%
rename from cer_pac/00001000000203285735.cer
rename to admincfdi/cer_pac/00001000000203285735.cer
diff --git a/cer_pac/00001000000203292609.cer b/admincfdi/cer_pac/00001000000203292609.cer
similarity index 100%
rename from cer_pac/00001000000203292609.cer
rename to admincfdi/cer_pac/00001000000203292609.cer
diff --git a/cer_pac/00001000000203312933.cer b/admincfdi/cer_pac/00001000000203312933.cer
similarity index 100%
rename from cer_pac/00001000000203312933.cer
rename to admincfdi/cer_pac/00001000000203312933.cer
diff --git a/cer_pac/00001000000203352843.cer b/admincfdi/cer_pac/00001000000203352843.cer
similarity index 100%
rename from cer_pac/00001000000203352843.cer
rename to admincfdi/cer_pac/00001000000203352843.cer
diff --git a/cer_pac/00001000000203392777.cer b/admincfdi/cer_pac/00001000000203392777.cer
similarity index 100%
rename from cer_pac/00001000000203392777.cer
rename to admincfdi/cer_pac/00001000000203392777.cer
diff --git a/cer_pac/00001000000203430011.cer b/admincfdi/cer_pac/00001000000203430011.cer
similarity index 100%
rename from cer_pac/00001000000203430011.cer
rename to admincfdi/cer_pac/00001000000203430011.cer
diff --git a/cer_pac/00001000000203495276.cer b/admincfdi/cer_pac/00001000000203495276.cer
similarity index 100%
rename from cer_pac/00001000000203495276.cer
rename to admincfdi/cer_pac/00001000000203495276.cer
diff --git a/cer_pac/00001000000203495475.cer b/admincfdi/cer_pac/00001000000203495475.cer
similarity index 100%
rename from cer_pac/00001000000203495475.cer
rename to admincfdi/cer_pac/00001000000203495475.cer
diff --git a/cer_pac/00001000000203631919.cer b/admincfdi/cer_pac/00001000000203631919.cer
similarity index 100%
rename from cer_pac/00001000000203631919.cer
rename to admincfdi/cer_pac/00001000000203631919.cer
diff --git a/cer_pac/00001000000300091673.cer b/admincfdi/cer_pac/00001000000300091673.cer
similarity index 100%
rename from cer_pac/00001000000300091673.cer
rename to admincfdi/cer_pac/00001000000300091673.cer
diff --git a/cer_pac/00001000000300171291.cer b/admincfdi/cer_pac/00001000000300171291.cer
similarity index 100%
rename from cer_pac/00001000000300171291.cer
rename to admincfdi/cer_pac/00001000000300171291.cer
diff --git a/cer_pac/00001000000300171326.cer b/admincfdi/cer_pac/00001000000300171326.cer
similarity index 100%
rename from cer_pac/00001000000300171326.cer
rename to admincfdi/cer_pac/00001000000300171326.cer
diff --git a/cer_pac/00001000000300209963.cer b/admincfdi/cer_pac/00001000000300209963.cer
similarity index 100%
rename from cer_pac/00001000000300209963.cer
rename to admincfdi/cer_pac/00001000000300209963.cer
diff --git a/cer_pac/00001000000300250292.cer b/admincfdi/cer_pac/00001000000300250292.cer
similarity index 100%
rename from cer_pac/00001000000300250292.cer
rename to admincfdi/cer_pac/00001000000300250292.cer
diff --git a/cer_pac/00001000000300392385.cer b/admincfdi/cer_pac/00001000000300392385.cer
similarity index 100%
rename from cer_pac/00001000000300392385.cer
rename to admincfdi/cer_pac/00001000000300392385.cer
diff --git a/cer_pac/00001000000300407877.cer b/admincfdi/cer_pac/00001000000300407877.cer
similarity index 100%
rename from cer_pac/00001000000300407877.cer
rename to admincfdi/cer_pac/00001000000300407877.cer
diff --git a/cer_pac/00001000000300439968.cer b/admincfdi/cer_pac/00001000000300439968.cer
similarity index 100%
rename from cer_pac/00001000000300439968.cer
rename to admincfdi/cer_pac/00001000000300439968.cer
diff --git a/cer_pac/00001000000300494998.cer b/admincfdi/cer_pac/00001000000300494998.cer
similarity index 100%
rename from cer_pac/00001000000300494998.cer
rename to admincfdi/cer_pac/00001000000300494998.cer
diff --git a/cer_pac/00001000000300627194.cer b/admincfdi/cer_pac/00001000000300627194.cer
similarity index 100%
rename from cer_pac/00001000000300627194.cer
rename to admincfdi/cer_pac/00001000000300627194.cer
diff --git a/cer_pac/00001000000300716418.cer b/admincfdi/cer_pac/00001000000300716418.cer
similarity index 100%
rename from cer_pac/00001000000300716418.cer
rename to admincfdi/cer_pac/00001000000300716418.cer
diff --git a/cer_pac/00001000000300716428.cer b/admincfdi/cer_pac/00001000000300716428.cer
similarity index 100%
rename from cer_pac/00001000000300716428.cer
rename to admincfdi/cer_pac/00001000000300716428.cer
diff --git a/cer_pac/00001000000300774022.cer b/admincfdi/cer_pac/00001000000300774022.cer
similarity index 100%
rename from cer_pac/00001000000300774022.cer
rename to admincfdi/cer_pac/00001000000300774022.cer
diff --git a/cer_pac/00001000000300915978.cer b/admincfdi/cer_pac/00001000000300915978.cer
similarity index 100%
rename from cer_pac/00001000000300915978.cer
rename to admincfdi/cer_pac/00001000000300915978.cer
diff --git a/cer_pac/00001000000300969660.cer b/admincfdi/cer_pac/00001000000300969660.cer
similarity index 100%
rename from cer_pac/00001000000300969660.cer
rename to admincfdi/cer_pac/00001000000300969660.cer
diff --git a/cer_pac/00001000000301021501.cer b/admincfdi/cer_pac/00001000000301021501.cer
similarity index 100%
rename from cer_pac/00001000000301021501.cer
rename to admincfdi/cer_pac/00001000000301021501.cer
diff --git a/cer_pac/00001000000301032322.cer b/admincfdi/cer_pac/00001000000301032322.cer
similarity index 100%
rename from cer_pac/00001000000301032322.cer
rename to admincfdi/cer_pac/00001000000301032322.cer
diff --git a/cer_pac/00001000000301062628.cer b/admincfdi/cer_pac/00001000000301062628.cer
similarity index 100%
rename from cer_pac/00001000000301062628.cer
rename to admincfdi/cer_pac/00001000000301062628.cer
diff --git a/cer_pac/00001000000301083052.cer b/admincfdi/cer_pac/00001000000301083052.cer
similarity index 100%
rename from cer_pac/00001000000301083052.cer
rename to admincfdi/cer_pac/00001000000301083052.cer
diff --git a/cer_pac/00001000000301100488.cer b/admincfdi/cer_pac/00001000000301100488.cer
similarity index 100%
rename from cer_pac/00001000000301100488.cer
rename to admincfdi/cer_pac/00001000000301100488.cer
diff --git a/cer_pac/00001000000301160463.cer b/admincfdi/cer_pac/00001000000301160463.cer
similarity index 100%
rename from cer_pac/00001000000301160463.cer
rename to admincfdi/cer_pac/00001000000301160463.cer
diff --git a/admincfdi/cer_pac/00001000000301205071.cer b/admincfdi/cer_pac/00001000000301205071.cer
new file mode 100644
index 0000000..b97bfe6
Binary files /dev/null and b/admincfdi/cer_pac/00001000000301205071.cer differ
diff --git a/admincfdi/cer_pac/00001000000301251152.cer b/admincfdi/cer_pac/00001000000301251152.cer
new file mode 100644
index 0000000..d094d92
Binary files /dev/null and b/admincfdi/cer_pac/00001000000301251152.cer differ
diff --git a/cer_pac/00001000000301280594.cer b/admincfdi/cer_pac/00001000000301280594.cer
similarity index 100%
rename from cer_pac/00001000000301280594.cer
rename to admincfdi/cer_pac/00001000000301280594.cer
diff --git a/cer_pac/00001000000301567711.cer b/admincfdi/cer_pac/00001000000301567711.cer
similarity index 100%
rename from cer_pac/00001000000301567711.cer
rename to admincfdi/cer_pac/00001000000301567711.cer
diff --git a/cer_pac/00001000000301634628.cer b/admincfdi/cer_pac/00001000000301634628.cer
similarity index 100%
rename from cer_pac/00001000000301634628.cer
rename to admincfdi/cer_pac/00001000000301634628.cer
diff --git a/cer_pac/00001000000301751173.cer b/admincfdi/cer_pac/00001000000301751173.cer
similarity index 100%
rename from cer_pac/00001000000301751173.cer
rename to admincfdi/cer_pac/00001000000301751173.cer
diff --git a/cer_pac/00001000000301914249.cer b/admincfdi/cer_pac/00001000000301914249.cer
similarity index 100%
rename from cer_pac/00001000000301914249.cer
rename to admincfdi/cer_pac/00001000000301914249.cer
diff --git a/cer_pac/00001000000301927035.cer b/admincfdi/cer_pac/00001000000301927035.cer
similarity index 100%
rename from cer_pac/00001000000301927035.cer
rename to admincfdi/cer_pac/00001000000301927035.cer
diff --git a/cer_pac/00001000000301949314.cer b/admincfdi/cer_pac/00001000000301949314.cer
similarity index 100%
rename from cer_pac/00001000000301949314.cer
rename to admincfdi/cer_pac/00001000000301949314.cer
diff --git a/cer_pac/00001000000304339685.cer b/admincfdi/cer_pac/00001000000304339685.cer
similarity index 100%
rename from cer_pac/00001000000304339685.cer
rename to admincfdi/cer_pac/00001000000304339685.cer
diff --git a/cer_pac/00001000000304691381.cer b/admincfdi/cer_pac/00001000000304691381.cer
similarity index 100%
rename from cer_pac/00001000000304691381.cer
rename to admincfdi/cer_pac/00001000000304691381.cer
diff --git a/cer_pac/20001000000100005868.cer b/admincfdi/cer_pac/20001000000100005868.cer
similarity index 100%
rename from cer_pac/20001000000100005868.cer
rename to admincfdi/cer_pac/20001000000100005868.cer
diff --git a/img/calc.gif b/admincfdi/img/calc.gif
similarity index 100%
rename from img/calc.gif
rename to admincfdi/img/calc.gif
diff --git a/img/calc.png b/admincfdi/img/calc.png
similarity index 100%
rename from img/calc.png
rename to admincfdi/img/calc.png
diff --git a/img/csv.gif b/admincfdi/img/csv.gif
similarity index 100%
rename from img/csv.gif
rename to admincfdi/img/csv.gif
diff --git a/img/csv.png b/admincfdi/img/csv.png
similarity index 100%
rename from img/csv.png
rename to admincfdi/img/csv.png
diff --git a/img/delete.gif b/admincfdi/img/delete.gif
similarity index 100%
rename from img/delete.gif
rename to admincfdi/img/delete.gif
diff --git a/img/delete.png b/admincfdi/img/delete.png
similarity index 100%
rename from img/delete.png
rename to admincfdi/img/delete.png
diff --git a/img/down.gif b/admincfdi/img/down.gif
similarity index 100%
rename from img/down.gif
rename to admincfdi/img/down.gif
diff --git a/img/down.png b/admincfdi/img/down.png
similarity index 100%
rename from img/down.png
rename to admincfdi/img/down.png
diff --git a/img/exit.gif b/admincfdi/img/exit.gif
similarity index 100%
rename from img/exit.gif
rename to admincfdi/img/exit.gif
diff --git a/img/exit.png b/admincfdi/img/exit.png
similarity index 100%
rename from img/exit.png
rename to admincfdi/img/exit.png
diff --git a/img/favicon.gif b/admincfdi/img/favicon.gif
similarity index 100%
rename from img/favicon.gif
rename to admincfdi/img/favicon.gif
diff --git a/img/favicon.png b/admincfdi/img/favicon.png
similarity index 100%
rename from img/favicon.png
rename to admincfdi/img/favicon.png
diff --git a/img/folder.gif b/admincfdi/img/folder.gif
similarity index 100%
rename from img/folder.gif
rename to admincfdi/img/folder.gif
diff --git a/img/folder.png b/admincfdi/img/folder.png
similarity index 100%
rename from img/folder.png
rename to admincfdi/img/folder.png
diff --git a/img/pdf.gif b/admincfdi/img/pdf.gif
similarity index 100%
rename from img/pdf.gif
rename to admincfdi/img/pdf.gif
diff --git a/img/pdf.png b/admincfdi/img/pdf.png
similarity index 100%
rename from img/pdf.png
rename to admincfdi/img/pdf.png
diff --git a/img/report.gif b/admincfdi/img/report.gif
similarity index 100%
rename from img/report.gif
rename to admincfdi/img/report.gif
diff --git a/img/report.png b/admincfdi/img/report.png
similarity index 100%
rename from img/report.png
rename to admincfdi/img/report.png
diff --git a/img/save.gif b/admincfdi/img/save.gif
similarity index 100%
rename from img/save.gif
rename to admincfdi/img/save.gif
diff --git a/img/save.png b/admincfdi/img/save.png
similarity index 100%
rename from img/save.png
rename to admincfdi/img/save.png
diff --git a/img/xml.gif b/admincfdi/img/xml.gif
similarity index 100%
rename from img/xml.gif
rename to admincfdi/img/xml.gif
diff --git a/img/xml.png b/admincfdi/img/xml.png
similarity index 100%
rename from img/xml.png
rename to admincfdi/img/xml.png
diff --git a/pyutil.py b/admincfdi/pyutil.py
similarity index 53%
rename from pyutil.py
rename to admincfdi/pyutil.py
index bbeceed..841794f 100644
--- a/pyutil.py
+++ b/admincfdi/pyutil.py
@@ -15,7 +15,6 @@
import sys
import re
import csv
-import glob
import json
import ftplib
import time
@@ -35,7 +34,16 @@
from tkinter.filedialog import askdirectory
from tkinter.filedialog import askopenfilename
from tkinter import messagebox
-from pysimplesoap.client import SoapClient, SoapFault
+from requests import Request, Session
+from selenium import webdriver
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.common.exceptions import TimeoutException
+from fpdf import FPDF
+from admincfdi.values import Global
+
+
try:
from subprocess import DEVNULL
except ImportError:
@@ -45,40 +53,63 @@
WIN = 'win32'
MAC = 'darwin'
LINUX = 'linux'
-APP_LIBO = True
+LIBO = True
if sys.platform == WIN:
try:
from win32com.client import Dispatch
- except ImportError as e:
- APP_LIBO = False
+ except ImportError:
+ LIBO = False
elif sys.platform == LINUX:
- import uno
- from com.sun.star.beans import PropertyValue
- from com.sun.star.beans.PropertyState import DIRECT_VALUE
- from com.sun.star.awt import Size
+ try:
+ import uno
+ from com.sun.star.beans import PropertyValue
+ from com.sun.star.beans.PropertyState import DIRECT_VALUE
+ from com.sun.star.awt import Size
+ except ImportError:
+ LIBO = False
class SAT(object):
_webservice = 'https://consultaqr.facturaelectronica.sat.gob.mx/' \
- 'consultacfdiservice.svc?wsdl'
-
- def __init__(self):
+ 'consultacfdiservice.svc'
+ _soap = """
+
+
+
+
+
+ ?re={emisor_rfc}&rr={receptor_rfc}&tt={total}&id={uuid}
+
+
+
+ """
+
+ def __init__(self, log):
+ self.log = log
self.error = ''
self.msg = ''
def get_estatus(self, data):
+ data = self._soap.format(**data).encode('utf-8')
+ headers = {
+ 'SOAPAction': '"http://tempuri.org/IConsultaCFDIService/Consulta"',
+ 'Content-length': len(data),
+ 'Content-type': 'text/xml; charset="UTF-8"'
+ }
+ s = Session()
+ req = Request('POST', self._webservice, data=data, headers=headers)
+ prepped = req.prepare()
try:
- args = '?re={emisor_rfc}&rr={receptor_rfc}&tt={total}&id={uuid}'
- client = SoapClient(wsdl = self._webservice)
- fac = args.format(**data)
- res = client.Consulta(fac)
- if 'ConsultaResult' in res:
- self.msg = res['ConsultaResult']['Estado']
- return True
- return False
- except SoapFault as sf:
- self.error = sf.faultstring
+ response = s.send(prepped, timeout=5)
+ tree = ET.fromstring(response.text)
+ self.msg = tree[0][0][0][1].text
+ return True
+ except Exception as e:
+ self.log.error(str(e))
return False
@@ -311,6 +342,9 @@ class Util(object):
def __init__(self):
self.OS = sys.platform
+ def call(self, *arg):
+ return subprocess.call(*arg)
+
def get_folder(self, parent):
return askdirectory(parent=parent)
@@ -334,6 +368,10 @@ def get_path_info(self, path, index=-1):
else:
return data[index]
+ def replace_extension(self, path, new_ext):
+ path, _, name, _ = self.get_path_info(path)
+ return self.join(path, name + new_ext)
+
def path_join(self, *paths):
return os.path.join(*paths)
@@ -343,11 +381,12 @@ def path_config(self, path):
target = path.replace('/', '\\')
return target
- def get_files(self, path, ext='*.xml'):
- files = []
- for folder,_,_ in os.walk(path):
- files.extend(glob.glob(os.path.join(folder, ext.lower())))
- return tuple(files)
+ def get_files(self, path, ext='xml'):
+ xmls = []
+ for folder, _, files in os.walk(path):
+ pattern = re.compile('\.{}'.format(ext), re.IGNORECASE)
+ xmls += [os.path.join(folder,f) for f in files if pattern.search(f)]
+ return tuple(xmls)
def join(self, *paths):
return os.path.join(*paths)
@@ -399,6 +438,22 @@ def sleep(self, sec=1):
time.sleep(sec)
return
+ def lee_credenciales(self, ruta_archivo):
+ '''Lee un renglón de ruta_archivo, regresa status, rfc, pwd
+
+ status es 'Ok', o un mensaje de error'''
+
+ try:
+ f = open(ruta_archivo)
+ except FileNotFoundError:
+ return 'Archivo no encontrado: ' + ruta_archivo, '', ''
+ row = f.readline().strip()
+ fields = row.split()
+ if not len(fields) == 2:
+ return 'No contiene dos campos: ' + ruta_archivo, '', ''
+ rfc, pwd = fields
+ return 'Ok', rfc, pwd
+
def load_config(self, path):
data = {}
try:
@@ -422,10 +477,13 @@ def save_config(self, path, key, value):
def now(self):
return datetime.now()
- def get_dates(self, year, month):
- days = calendar.monthrange(year, month)[1]
- d1 = '01/{:02d}/{}'.format(month, year)
- d2 = '{}/{:02d}/{}'.format(days, month, year)
+ def get_dates(self, year, month, day=0):
+ if day:
+ d1 = d2 = '{:02d}/{:02d}/{}'.format(day, month, year)
+ else:
+ days = calendar.monthrange(year, month)[1]
+ d1 = '01/{:02d}/{}'.format(month, year)
+ d2 = '{}/{:02d}/{}'.format(days, month, year)
return d1, d2
def get_days(self, year, month):
@@ -509,9 +567,7 @@ def get_name(self, path, PRE, format1='', format2=''):
if not 'serie' in data:
data['serie'] = ''
data['fecha'] = data['fecha'].partition('T')[0]
- if 'folio' in data:
- data['folio'] = int(data['folio'])
- else:
+ if not 'folio' in data:
data['folio'] = 0
node = xml.find('{}Emisor'.format(pre))
data['emisor_rfc'] = node.attrib['rfc']
@@ -671,7 +727,7 @@ def get_info_report(self, path, options, g):
'total': data['total'],
'uuid': data['UUID']
}
- sat = SAT()
+ sat = SAT(g.LOG)
if sat.get_estatus(data_sat):
info.append(sat.msg)
else:
@@ -1653,3 +1709,1124 @@ def _format_date(self, date_string):
return date.strftime('{}, %d de {} de %Y'.format(
d[date.weekday()], m[date.month]))
+class visibility_of_either(object):
+ """ An expectation for checking that one of two elements,
+ located by locator 1 and locator2, is visible.
+ Visibility means that the element is not only
+ displayed but also has a height and width that is
+ greater than 0.
+ returns the WebElement that is visible.
+ """
+ def __init__(self, locator1, locator2):
+ self.locator1 = locator1
+ self.locator2 = locator2
+
+ def __call__(self, driver):
+ element1 = driver.find_element(*self.locator1)
+ element2 = driver.find_element(*self.locator2)
+ return _element_if_visible(element1) or \
+ _element_if_visible(element2)
+
+def _element_if_visible(element):
+ return element if element.is_displayed() else False
+
+
+class DescargaSAT(object):
+
+ def __init__(self, status_callback=print,
+ download_callback=print):
+ self.g = Global()
+ self.util = Util()
+ self.status = status_callback
+ self.progress = download_callback
+ self.browser = None
+
+ def get_firefox_profile(self, carpeta_destino):
+ 'Devuelve un perfil para Firefox'
+
+ # To prevent download dialog
+ profile = webdriver.FirefoxProfile()
+ profile.set_preference(
+ 'browser.download.folderList', 2)
+ profile.set_preference(
+ 'browser.download.manager.showWhenStarting', False)
+ profile.set_preference(
+ 'browser.helperApps.alwaysAsk.force', False)
+ profile.set_preference(
+ 'browser.helperApps.neverAsk.saveToDisk',
+ 'text/xml, application/octet-stream, application/xml')
+ profile.set_preference(
+ 'browser.download.dir', carpeta_destino)
+ # mrE - desactivar telemetry
+ profile.set_preference(
+ 'toolkit.telemetry.prompted', 2)
+ profile.set_preference(
+ 'toolkit.telemetry.rejected', True)
+ profile.set_preference(
+ 'toolkit.telemetry.enabled', False)
+ profile.set_preference(
+ 'datareporting.healthreport.service.enabled', False)
+ profile.set_preference(
+ 'datareporting.healthreport.uploadEnabled', False)
+ profile.set_preference(
+ 'datareporting.healthreport.service.firstRun', False)
+ profile.set_preference(
+ 'datareporting.healthreport.logging.consoleEnabled', False)
+ profile.set_preference(
+ 'datareporting.policy.dataSubmissionEnabled', False)
+ profile.set_preference(
+ 'datareporting.policy.dataSubmissionPolicyResponseType', 'accepted-info-bar-dismissed')
+ #profile.set_preference(
+ # 'datareporting.policy.dataSubmissionPolicyAccepted'; False) # este me marca error, why?
+ #oculta la gran flecha animada al descargar
+ profile.set_preference(
+ 'browser.download.animateNotifications', False)
+ return profile
+
+ def connect(self, profile, rfc='', ciec=''):
+ 'Lanza navegador y hace login en el portal del SAT'
+
+ self.status('Abriendo Firefox...')
+ browser = webdriver.Firefox(profile)
+ self.browser = browser
+ self.status('Conectando...')
+ browser.get(self.g.SAT['page_init'])
+ txt = browser.find_element_by_name(self.g.SAT['user'])
+ txt.send_keys(rfc)
+ txt = browser.find_element_by_name(self.g.SAT['password'])
+ txt.send_keys(ciec)
+ txt.submit()
+ wait = WebDriverWait(browser, 10)
+ try:
+ wait.until(EC.title_contains('NetIQ Access'))
+ self.status('Conectado')
+ return True
+ except TimeoutException:
+ self.status('No conectado')
+ return False
+
+ def disconnect(self):
+ 'Cierra la sesión y el navegador'
+
+ if self.browser:
+ try:
+ self.status('Desconectando...')
+ link = self.browser.find_element_by_partial_link_text('Cerrar Sesi')
+ link.click()
+ except:
+ pass
+ finally:
+ self.browser.close()
+ self.status('Desconectado...')
+ self.browser = None
+
+ def search(self,
+ facturas_emitidas=False,
+ uuid='',
+ rfc_emisor='',
+ año=None,
+ mes=None,
+ día='00',
+ hora_inicial='0',
+ minuto_inicial='0',
+ segundo_inicial='0',
+ hora_final='23',
+ minuto_final='59',
+ segundo_final='59',
+ mes_completo_por_día=False):
+ 'Busca y regresa los resultados'
+
+ if self.browser:
+ browser = self.browser
+
+ page_query = self.g.SAT['page_receptor']
+ if facturas_emitidas:
+ page_query = self.g.SAT['page_emisor']
+
+ browser.get(page_query)
+ wait = WebDriverWait(browser, 15)
+ wait.until(EC.title_contains('Buscar CFDI'))
+ self.status('Buscando...')
+ if uuid:
+ txt = browser.find_element_by_id(self.g.SAT['uuid'])
+ txt.click()
+ txt.send_keys(uuid)
+ else:
+ # Descargar por fecha
+ opt = browser.find_element_by_id(self.g.SAT['date'])
+ opt.click()
+ wait.until(EC.staleness_of(opt))
+ if facturas_emitidas:
+ txt = wait.until(EC.element_to_be_clickable(
+ (By.ID, self.g.SAT['receptor'])))
+ else:
+ txt = wait.until(EC.element_to_be_clickable(
+ (By.ID, self.g.SAT['emisor'])))
+ if rfc_emisor:
+ txt.send_keys(rfc_emisor)
+
+ hora_inicial = int(hora_inicial)
+ minuto_inicial = int(minuto_inicial)
+ segundo_inicial = int(segundo_inicial)
+ hora_final = int(hora_final)
+ minuto_final = int(minuto_final)
+ segundo_final = int(segundo_final)
+
+ # Emitidas
+ if facturas_emitidas:
+ year = int(año)
+ month = int(mes)
+ day = int(día)
+ if not mes_completo_por_día and day:
+ dates = self.util.get_dates(year, month, day)
+ else:
+ dates = self.util.get_dates(year, month)
+ txt = browser.find_element_by_id(self.g.SAT['date_from'])
+ arg = "document.getElementsByName('{}')[0]." \
+ "removeAttribute('disabled');".format(
+ self.g.SAT['date_from_name'])
+ browser.execute_script(arg)
+ txt.send_keys(dates[0])
+ txt = browser.find_element_by_id(self.g.SAT['date_to'])
+ arg = "document.getElementsByName('{}')[0]." \
+ "removeAttribute('disabled');".format(
+ self.g.SAT['date_to_name'])
+ browser.execute_script(arg)
+ txt.send_keys(dates[1])
+
+ # Hay que seleccionar también la hora, minuto y segundo
+ arg = "document.getElementById('{}')." \
+ "value={};".format(
+ self.g.SAT['hour'], hora_final)
+ browser.execute_script(arg)
+ arg = "document.getElementById('{}')." \
+ "value={};".format(
+ self.g.SAT['minute'], minuto_final)
+ browser.execute_script(arg)
+ arg = "document.getElementById('{}')." \
+ "value={};".format(
+ self.g.SAT['second'], segundo_final)
+ browser.execute_script(arg)
+ if mes_completo_por_día:
+ return self._download_sat_month_emitidas(dates[1], day)
+ # Recibidas
+ else:
+ arg = "document.getElementById('{}')." \
+ "value={};".format(
+ self.g.SAT['year'], año)
+ browser.execute_script(arg)
+ arg = "document.getElementById('{}')." \
+ "value={};".format(
+ self.g.SAT['month'], mes)
+ browser.execute_script(arg)
+ arg = "document.getElementById('{}')." \
+ "value='{}';".format(
+ self.g.SAT['day'], día)
+ browser.execute_script(arg)
+ arg = "document.getElementById('{}')." \
+ "value='{}';".format(
+ self.g.SAT['start_hour'], hora_inicial)
+ browser.execute_script(arg)
+ arg = "document.getElementById('{}')." \
+ "value='{}';".format(
+ self.g.SAT['start_minute'], minuto_inicial)
+ browser.execute_script(arg)
+ arg = "document.getElementById('{}')." \
+ "value='{}';".format(
+ self.g.SAT['start_second'], segundo_inicial)
+ browser.execute_script(arg)
+ arg = "document.getElementById('{}')." \
+ "value='{}';".format(
+ self.g.SAT['end_hour'], hora_final)
+ browser.execute_script(arg)
+ arg = "document.getElementById('{}')." \
+ "value='{}';".format(
+ self.g.SAT['end_minute'], minuto_final)
+ browser.execute_script(arg)
+ arg = "document.getElementById('{}')." \
+ "value='{}';".format(
+ self.g.SAT['end_second'], segundo_final)
+ browser.execute_script(arg)
+
+ results_table = browser.find_element_by_id(
+ self.g.SAT['resultados'])
+ browser.find_element_by_id(self.g.SAT['submit']).click()
+ wait.until(EC.staleness_of(results_table))
+ wait.until(visibility_of_either(
+ (By.ID, self.g.SAT['resultados']),
+ (By.ID, self.g.SAT['noresultados'])))
+ if not facturas_emitidas and mes_completo_por_día:
+ return self._download_sat_month(año, mes, browser)
+
+ results = wait.until(visibility_of_either(
+ (By.ID, self.g.SAT['resultados']),
+ (By.ID, self.g.SAT['noresultados'])))
+ if results.get_attribute('id') == self.g.SAT['resultados']:
+ wait.until(EC.element_to_be_clickable(
+ (By.NAME, self.g.SAT['download'])))
+ docs = browser.find_elements_by_name(self.g.SAT['download'])
+ return docs
+ else:
+ self.status('Sin facturas...')
+ return []
+
+ def download(self, docs):
+ 'Descarga los resultados'
+ if docs is None:
+ self.status('No se encontraron documentos')
+ return
+ if self.browser:
+ t = len(docs)
+ for i, v in enumerate(docs):
+ msg = 'Factura {} de {}'.format(i+1, t)
+ self.progress(i + 1, t)
+ self.status(msg)
+ download = self.g.SAT['page_cfdi'].format(
+ v.get_attribute('onclick').split("'")[1])
+ self.browser.get(download)
+ self.progress(0, t)
+ self.util.sleep()
+ return
+
+ def _download_sat_month(self, año, mes, browser):
+ '''Descarga CFDIs del SAT a una carpeta local
+
+ Todos los CFDIs recibidos del mes selecionado'''
+
+ year = int(año)
+ month = int(mes)
+ days_month = self.util.get_days(year, month) + 1
+ days = ['%02d' % x for x in range(1, days_month)]
+ for d in days:
+ combo = browser.find_element_by_id(self.g.SAT['day'])
+ sb = combo.get_attribute('sb')
+ combo = browser.find_element_by_id('sbToggle_{}'.format(sb))
+ combo.click()
+ self.util.sleep(2)
+ if mes == d:
+ links = browser.find_elements_by_link_text(d)
+ for l in links:
+ p = l.find_element_by_xpath(
+ '..').find_element_by_xpath('..')
+ sb2 = p.get_attribute('id')
+ if sb in sb2:
+ link = l
+ break
+ else:
+ link = browser.find_element_by_link_text(d)
+ link.click()
+ self.util.sleep(2)
+ browser.find_element_by_id(self.g.SAT['submit']).click()
+ self.util.sleep(3)
+ docs = browser.find_elements_by_name(self.g.SAT['download'])
+ if docs:
+ t = len(docs)
+ for i, v in enumerate(docs):
+ msg = 'Factura {} de {}'.format(i+1, t)
+ self.progress(i + 1, t)
+ self.status(msg)
+ download = self.g.SAT['page_cfdi'].format(
+ v.get_attribute('onclick').split("'")[1])
+ browser.get(download)
+ self.util.sleep()
+ self.progress(0, t)
+ self.util.sleep()
+ return
+
+ def _download_sat_month_emitidas(self, date_end, day_init):
+ '''Descarga CFDIs del SAT a una carpeta local
+
+ Todos los CFDIs emitidos del mes selecionado
+ La interfaz en el SAT para facturas emitidas es diferente que para las
+ recibidas, es necesario buscar otros controles.
+ '''
+ if day_init > 0:
+ day_init -= 1
+ last_day = int(date_end.split('/')[0])
+ for day in range(day_init, last_day):
+ current = '{:02d}'.format(day + 1) + date_end[2:]
+ print ("Día: ", current)
+ arg = "document.getElementsByName('{}')[0].removeAttribute(" \
+ "'disabled');".format(self.g.SAT['date_from_name'])
+ self.browser.execute_script(arg)
+ arg = "document.getElementsByName('{}')[0].removeAttribute(" \
+ "'disabled');".format(self.g.SAT['date_to_name'])
+ self.browser.execute_script(arg)
+ date_from = self.browser.find_element_by_id(self.g.SAT['date_from'])
+ date_to = self.browser.find_element_by_id(self.g.SAT['date_to'])
+ date_from.clear()
+ date_to.clear()
+ date_from.send_keys(current)
+ date_to.send_keys(current)
+ self.browser.find_element_by_id(self.g.SAT['submit']).click()
+ self.util.sleep(5)
+ docs = self.browser.find_elements_by_name(self.g.SAT['download'])
+ if docs:
+ print ("\tDocumentos: ", len(docs))
+ else:
+ print ("\tDocumentos: 0")
+ self.download(docs)
+ return
+
+
+class CSVPDF(FPDF):
+ """
+ Genera un PDF a partir de un CSV
+ """
+ G = Global()
+ TITULO1 = {
+ '2.0': 'Datos CFD',
+ '2.2': 'Datos CFD',
+ '3.0': 'Datos CFDI',
+ '3.2': 'Datos CFDI'
+ }
+ TITULO2 = {
+ '2.0': 'Año Aprobación:',
+ '2.2': 'Año Aprobación:',
+ '3.0': 'Serie CSD SAT:',
+ '3.2': 'Serie CSD SAT:'
+ }
+ TITULO3 = {
+ '2.0': 'N° Aprobación:',
+ '2.2': 'N° Aprobación:',
+ '3.0': 'Folio Fiscal:',
+ '3.2': 'Folio Fiscal:'
+ }
+ SPACE = 8
+ LIMIT_MARGIN = 260
+ DECIMALES = 2
+
+ def __init__(self, path_xml, path_template='', status_callback=print):
+ super().__init__(format='Letter')
+ self.status = status_callback
+ try:
+ self.xml = ET.parse(path_xml).getroot()
+ self.status('Generando: {}'.format(path_xml))
+ except Exception as e:
+ self.xml = None
+ self.status('Error al parsear: {}'.format(path_xml))
+ self.status(str(e))
+ self.compress = False
+ self.error = ''
+ self.set_auto_page_break(True, margin=15)
+ self.SetRightMargin = 15
+ self.SetTopMargin = 5
+ self.set_draw_color(50, 50, 50)
+ self.set_title('Factura Libre')
+ self.set_author('www.facturalibre.net')
+ self.line_width = 0.1
+ self.alias_nb_pages()
+ self.pos_size = {'x': 0, 'y': 0, 'w': 0, 'h': 0}
+ self.y_detalle = 80
+ self.data = {}
+ self.elements = {}
+ decimales = self.DECIMALES
+ self.COLOR_RED = self._color(255, 0, 0)
+ if self.xml:
+ self.version = self.xml.attrib['version']
+ #~ self.cadena = self._get_cadena(path_xml)
+ self._parse_csv(path_template)
+
+ try:
+ decimales = len(self.xml.attrib['total'].split('.')[1])
+ except IndexError:
+ decimales = self.DECIMALES
+ if decimales == 1:
+ decimales = self.DECIMALES
+
+ self.currency = '{0:,.%sf}' % decimales
+ self.monedas = {
+ 'peso': ('peso', '$', 'm.n.'),
+ 'pesos': ('peso', '$', 'm.n.'),
+ 'mxn': ('peso', '$', 'm.n.'),
+ 'mxp': ('peso', '$', 'm.n.'),
+ 'euro': ('euro', '€', '€'),
+ 'euros': ('euro', '€', '€'),
+ 'dolar': ('dolar', '$', 'usd'),
+ 'dolares': ('dolar', '$', 'usd'),
+ 'usd': ('dolar', '$', 'usd'),
+ }
+ self.timbre = ''
+
+ def make_pdf(self):
+ self.add_page()
+ self._set_detalle(self.G.PREFIX[self.version])
+ self._set_totales(self.G.PREFIX[self.version])
+ self._set_comprobante2(self.G.PREFIX[self.version])
+ return
+
+ def header(self):
+ self._set_emisor(self.G.PREFIX[self.version])
+ self._set_receptor(self.G.PREFIX[self.version])
+ self._set_comprobante(self.G.PREFIX[self.version])
+ self._set_encabezados_detalle()
+ pass
+
+ def footer(self):
+ self.set_y(-15)
+ self.set_font('Helvetica', '', 8)
+ self.set_text_color(*self._rgb(0))
+ self.cell(0, 10, 'Página %s de {nb}' % self.page_no(), 0, 0, 'R')
+ self.set_x(10)
+ s = 'Factura elaborada con software libre '
+ self.cell(0, 10, s, 0, 0, 'L')
+ self.set_x(10 + self.get_string_width(s))
+ self.set_text_color(*self._rgb(6291456))
+ self.cell(0, 10, 'www.facturalibre.net', 0, 0, 'L',
+ link='www.facturalibre.net')
+
+ def _verify_margin(self, h=0):
+ mb = self.y + h
+ if mb > self.LIMIT_MARGIN:
+ self.add_page()
+ self.set_y(self.y_detalle)
+ return
+
+ def _set_leyendas(self, pre):
+ com = self.xml.find('%sComplemento' % pre)
+ if com is None:
+ return
+ nodo = com.find('%sLeyendasFiscales' % self.G.PREFIX['LEYENDAS'])
+ if nodo is None:
+ return
+ for leyenda in list(nodo):
+ if 'disposicionFiscal' in leyenda.attrib:
+ self.elements['fiscal1_titulo']['y'] = self.y
+ self.elements['fiscal1']['y'] = self.y
+ self._write_text('fiscal1_titulo')
+ self._write_text('fiscal1', leyenda.attrib['disposicionFiscal'])
+ if 'norma' in leyenda.attrib:
+ self.elements['fiscal2_titulo']['y'] = self.y
+ self.elements['fiscal2']['y'] = self.y
+ self._write_text('fiscal2_titulo')
+ self._write_text('fiscal2', leyenda.attrib['norma'])
+ self.elements['fiscal_leyenda']['y'] = self.y + 5
+ self._write_text('fiscal_leyenda', leyenda.attrib['textoLeyenda'])
+ self.ln(self.SPACE)
+ return
+
+ def _set_donataria(self, pre):
+ com = self.xml.find('%sComplemento' % pre)
+ if com is None: return
+ nodo = com.find('%sDonatarias' % self.G.PREFIX['DONATARIA'])
+ if nodo is None: return
+ self.elements['dona_aut_titulo']['y'] = self.y
+ self.elements['dona_aut']['y'] = self.y
+ self._write_text('dona_aut_titulo')
+ self._write_text('dona_aut', nodo.attrib['noAutorizacion'])
+ self.elements['dona_fecha_titulo']['y'] = self.y
+ self.elements['dona_fecha']['y'] = self.y
+ self._write_text('dona_fecha_titulo')
+ self._write_text('dona_fecha', nodo.attrib['fechaAutorizacion'])
+ self.elements['dona_leyenda']['y'] = self.y + 5
+ self._write_text('dona_leyenda', nodo.attrib['leyenda'])
+ self.ln(self.SPACE)
+ return
+
+ def _set_comprobante2(self, pre):
+ fields = (
+ ('Moneda', 'Moneda'),
+ ('TipoCambio', u'Tipo de cambio'),
+ ('formaDePago', u'Forma de pago'),
+ ('condicionesDePago', u'Condiciones de pago'),
+ ('metodoDePago', u'Método de pago'),
+ ('NumCtaPago', u'Cuenta de pago'),
+ )
+ self._verify_margin(20)
+ self._set_donataria(pre)
+ self._verify_margin(20)
+ self._set_leyendas(pre)
+ self._verify_margin(20)
+ for f in fields:
+ if f[0] in self.xml.attrib:
+ self._write_otros(f[1], self.xml.attrib[f[0]])
+ self.ln(self.SPACE)
+ if self.timbre:
+ qr_data = '?re=%s&rr=%s&tt=%s&id=%s' % (
+ self.data['emisor_rfc'],
+ self.data['receptor_rfc'],
+ '%017.06f' % float(self.xml.attrib['total']),
+ self.timbre['UUID']
+ )
+ path = self._get_cbb(qr_data)
+ if os.path.exists(path):
+ self._verify_margin(self.elements['qr_cbb']['h'])
+ self.image(
+ path,
+ self.elements['qr_cbb']['x'],
+ self.y,
+ self.elements['qr_cbb']['w'],
+ self.elements['qr_cbb']['h']
+ )
+ os.unlink(path)
+ sello_emisor = self.timbre['selloCFD']
+ sello_sat = self.timbre['selloSAT']
+ self.elements['sello_cfd_titulo']['y'] = self.y
+ self.elements['sello_cfd_titulo']['text'] += 'I'
+ self._write_text('sello_cfd_titulo')
+ self.elements['sello_cfd']['y'] = self.y + 4
+ self._write_text('sello_cfd', sello_emisor)
+ self.elements['sello_sat_titulo']['y'] = self.y + 1
+ self._write_text('sello_sat_titulo')
+ self.elements['sello_sat']['y'] = self.y + 4
+ self._write_text('sello_sat', sello_sat)
+ self.elements['fecha_titulo']['y'] = self.y + 1
+ self._write_text('fecha_titulo')
+ self.elements['fecha']['y'] = self.y
+ self._write_text('fecha', self.timbre['FechaTimbrado'])
+ self.elements['leyenda']['text'] += 'I'
+ else:
+ sello_emisor = self.xml.attrib['sello']
+ self.elements['sello_cfd_titulo']['x'] = 10
+ self.elements['sello_cfd_titulo']['y'] = self.y
+ self._write_text('sello_cfd_titulo')
+ self.elements['sello_cfd']['x'] = 10
+ self.elements['sello_cfd']['y'] = self.y + 4
+ self.elements['sello_cfd']['w'] = 195
+ self.elements['sello_cfd']['multiline'] = 0
+ self._write_text('sello_cfd', sello_emisor)
+ self.elements['cadena_titulo']['text'] = 'Cadena original del CFD'
+ self._verify_margin(10)
+ self.elements['cadena_titulo']['y'] = self.y + 5
+ self._write_text('cadena_titulo')
+ self.elements['cadena']['y'] = self.y + 4
+ self._write_text('cadena', self._get_cadena())
+ #~ self._verify_margin(10)
+ self.elements['leyenda']['y'] = self.y + 5
+ self._write_text('leyenda')
+ return
+
+ def _get_cbb(self, data):
+ scale = 10
+ f = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
+ path = f.name
+ code = pyqrcode.QRCode(data, mode='binary')
+ code.png(path, scale)
+ return path
+
+ def _set_totales(self, pre):
+ moneda = ('peso', '$', 'm.n.')
+ if 'Moneda' in self.xml.attrib:
+ if self.xml.attrib['Moneda'].lower() in self.monedas:
+ moneda = self.monedas[self.xml.attrib['Moneda'].lower()]
+ else:
+ moneda = (self.xml.attrib['Moneda'], '', '')
+ importe = '%s %s' % (
+ moneda[1],
+ self.currency.format(float(self.xml.attrib['subTotal']))
+ )
+ self._verify_margin(20)
+ self._write_importe('Subtotal', importe)
+
+ if 'descuento' in self.xml.attrib:
+ if 'motivoDescuento' in self.xml.attrib:
+ self.elements['motivo_descuento']['y'] = self.y
+ self.elements['motivo_titulo']['y'] = self.y
+ self._write_text('motivo_titulo')
+ self._write_text(
+ 'motivo_descuento', self.xml.attrib['motivoDescuento'])
+ importe = '%s %s' % (
+ moneda[1],
+ self.currency.format(float(self.xml.attrib['descuento']))
+ )
+ self._write_importe('Descuento', importe)
+
+ imp = self.xml.find('%sImpuestos' % pre)
+ if imp is not None:
+ nodo = imp.find('%sTraslados' % pre)
+ if nodo is not None:
+ for n in list(nodo):
+ title = '%s %s %%' % (n.attrib['impuesto'], n.attrib['tasa'])
+ importe = '%s %s' % (
+ moneda[1],
+ self.currency.format(float(n.attrib['importe']))
+ )
+ self._write_importe(title, importe)
+ nodo = imp.find('%sRetenciones' % pre)
+ if nodo is not None:
+ for n in list(nodo):
+ title = u'Retención %s' % n.attrib['impuesto']
+ importe = '%s %s' % (
+ moneda[1],
+ self.currency.format(float(n.attrib['importe']))
+ )
+ self._write_importe(title, importe)
+
+ com = self.xml.find('%sComplemento' % pre)
+ if com is not None:
+ otros = com.find('%sImpuestosLocales' % self.G.PREFIX['IMP_LOCAL'])
+ if otros is not None:
+ for otro in list(otros):
+ if otro.tag == '%sRetencionesLocales' % self.G.PREFIX['IMP_LOCAL']:
+ name = 'ImpLocRetenido'
+ tasa = 'TasadeRetencion'
+ else:
+ name = 'ImpLocTrasladado'
+ tasa = 'TasadeTraslado'
+ title = u'%s %s %%' % (otro.attrib[name], otro.attrib[tasa])
+ importe = '%s %s' % (
+ moneda[1],
+ self.currency.format(float(otro.attrib['Importe']))
+ )
+ self._write_importe(title, importe)
+
+ if 'totalImpuestosTrasladados' in imp.attrib:
+ importe = '%s %s' % (
+ moneda[1],
+ self.currency.format(float(imp.attrib['totalImpuestosTrasladados']))
+ )
+ self.elements['imp_tras_titulo']['y'] = self.y
+ self.elements['imp_trasladado']['y'] = self.y
+ self._write_text('imp_tras_titulo')
+ self._write_text('imp_trasladado', importe)
+ if 'totalImpuestosRetenidos' in imp.attrib:
+ importe = '%s %s' % (
+ moneda[1],
+ self.currency.format(float(imp.attrib['totalImpuestosRetenidos']))
+ )
+ self.elements['imp_rete_titulo']['y'] = self.y
+ self.elements['imp_retenido']['y'] = self.y
+ self._write_text('imp_rete_titulo')
+ self._write_text('imp_retenido', importe)
+
+ total = float(self.xml.attrib['total'])
+ importe = '%s %s' % (
+ moneda[1],
+ self.currency.format(total)
+ )
+ self._write_importe('Total', importe)
+ self.ln()
+ letras = '-(%s/100 %s)-' % (
+ NumerosLetras().to_letters(total, moneda[0]),
+ moneda[2]
+ )
+ self._verify_margin(20)
+ self.elements['en_letras']['y'] = self.y
+ self._write_text('en_letras', letras.upper())
+ self.ln(self.SPACE)
+ return
+
+ def _write_otros(self, title, value):
+ self.elements['otros_titulo']['text'] = title
+ self.elements['otros_titulo']['y'] = self.y
+ self.elements['otros']['y'] = self.y
+ self._write_text('otros_titulo')
+ self._write_text('otros', value)
+ self.y += 3
+ return
+
+ def _write_importe(self, title, importe):
+ self.elements['subtotal_titulo']['text'] = title
+ self.elements['subtotal_titulo']['y'] = self.y
+ self.elements['subtotal']['y'] = self.y
+ self._write_text('subtotal_titulo')
+ self._write_text('subtotal', importe)
+ self.y += 4.5
+ return
+
+ def _set_detalle(self, pre):
+ conceptos = self.xml.find('%sConceptos' % pre)
+ for c in list(conceptos):
+ clave = ''
+ if 'noIdentificacion' in c.attrib:
+ clave = c.attrib['noIdentificacion']
+ self._write_text('clave', clave)
+ unidad = 'No aplica'
+ if 'unidad' in c.attrib:
+ unidad = c.attrib['unidad']
+ self._write_text('unidad', unidad)
+ importe = self.currency.format(float(c.attrib['cantidad']))
+ self._write_text('cantidad', importe)
+ importe = self.currency.format(float(c.attrib['valorUnitario']))
+ if c.attrib['valorUnitario'].startswith('-'):
+ self.elements['pu']['foreground'] = self.COLOR_RED
+ else:
+ self.elements['pu']['foreground'] = 0
+ self._write_text('pu', importe)
+ importe = self.currency.format(float(c.attrib['importe']))
+ if c.attrib['importe'].startswith('-'):
+ self.elements['importe']['foreground'] = self.COLOR_RED
+ else:
+ self.elements['importe']['foreground'] = 0
+ self._write_text('importe', importe)
+ page = self.page_no()
+ descripcion = self._get_descripcion(c, pre)
+ self._write_text('descripcion', descripcion)
+ if self.page_no() > page:
+ new_y = self.y_detalle
+ else:
+ new_y = self.y
+ self.elements['clave']['y'] = new_y
+ self.elements['descripcion']['y'] = new_y
+ self.elements['unidad']['y'] = new_y
+ self.elements['cantidad']['y'] = new_y
+ self.elements['pu']['y'] = new_y
+ self.elements['importe']['y'] = new_y
+ self.line(10, self.y, 205, self.y)
+ self.ln()
+ return
+
+ def _get_descripcion(self, nodo, pre):
+ s = nodo.attrib['descripcion']
+ for n in list(nodo):
+ if n.tag == '%sInformacionAduanera' % pre:
+ s += u'\nAduana: %s\nFecha: %s Número: %s' % (
+ n.attrib['aduana'],
+ n.attrib['fecha'],
+ n.attrib['numero']
+ )
+ elif n.tag == '%sCuentaPredial' % pre:
+ s += u'\n\nCuenta Predial Número: %s' % n.attrib['numero']
+ elif n.tag == '%sParte' % pre:
+ serie = ''
+ if 'noIdentificacion' in n.attrib:
+ serie = n.attrib['noIdentificacion']
+ s += u'\n\n Cantidad: %s Serie: %s\n %s' % (
+ n.attrib['cantidad'],
+ serie,
+ n.attrib['descripcion']
+ )
+ for n2 in list(n):
+ if n2.tag == '%sInformacionAduanera' % pre:
+ s += u'\n Aduana: %s\n Fecha: %s Número: %s' % (
+ n2.attrib['aduana'],
+ n2.attrib['fecha'],
+ n2.attrib['numero']
+ )
+ info = nodo.find('%sComplementoConcepto' % pre)
+ if info is None:
+ return s
+ info = info.find('{}instEducativas'.format(self.G.PREFIX['IEDU']))
+ if info is not None:
+ s1 = ''
+ if 'nombreAlumno' in info.attrib:
+ s += '\n\nAlumno: %s' % info.attrib['nombreAlumno']
+ if 'CURP' in info.attrib:
+ s += '\nCURP: %s' % info.attrib['CURP']
+ if 'nivelEducativo' in info.attrib:
+ s1 = '\nAcuerdo de incorporación ante la SEP %s' % info.attrib['nivelEducativo']
+ if 'autRVOE' in info.attrib:
+ if s1:
+ s1 = '%s %s' % (s1, info.attrib['autRVOE'])
+ else:
+ s1 = '\nAcuerdo de incorporación ante la SEP No: %s' % info.attrib['autRVOE']
+ s += s1
+ if 'rfcPago' in info.attrib:
+ s += '\nRFC de pago: %s' % info.attrib['rfcPago']
+ return s
+
+ def _set_encabezados_detalle(self):
+ self._write_text('clave_titulo')
+ self._write_text('descripcion_titulo')
+ self._write_text('unidad_titulo')
+ self._write_text('cantidad_titulo')
+ self._write_text('pu_titulo')
+ self._write_text('importe_titulo')
+ self.ln()
+ return
+
+ def _set_comprobante(self, pre):
+ if 'LugarExpedicion' in self.xml.attrib:
+ lugar = '%s, ' % self.xml.attrib['LugarExpedicion']
+ else:
+ lugar = self.data['expedicion']
+ fecha = self.xml.attrib['fecha'].split('T')
+ date = datetime.strptime(fecha[0], '%Y-%m-%d')
+ lugar += date.strftime('%A, %d de %B del %Y') # .decode('utf-8')
+ self._write_text('cfdi_fecha', lugar)
+ self._write_text('cfdi_hora', fecha[1])
+ self._write_text('cfdi_titulo1')
+ self._write_text('cfdi_titulo2', self.TITULO2[self.version])
+ self._write_text('cfdi_titulo3', self.TITULO3[self.version])
+ self._write_text('cfdi_titulo4')
+ self._write_text('cfdi_titulo5')
+ self._write_text('cfdi_titulo', self.TITULO1[self.version])
+ self._write_text('cfdi_regimen', self.data['regimen'])
+ self.timbre = self._get_timbre(self.G.PREFIX[self.version])
+ csdsat = ''
+ uuid = ''
+ if self.timbre:
+ csdsat = self.timbre['noCertificadoSAT']
+ uuid = self.timbre['UUID'].upper()
+ else:
+ if 'anoAprobacion' in self.xml.attrib:
+ csdsat = self.xml.attrib['anoAprobacion']
+ if 'noAprobacion' in self.xml.attrib:
+ uuid = self.xml.attrib['noAprobacion']
+ folio = ''
+ if 'serie' in self.xml.attrib:
+ folio += self.xml.attrib['serie']
+ if 'folio' in self.xml.attrib:
+ if folio:
+ folio += '-%s' % self.xml.attrib['folio']
+ else:
+ folio = self.xml.attrib['folio']
+ self._write_text('cfdi_csd', self.xml.attrib['noCertificado'])
+ self._write_text('cfdi_csdsat', csdsat)
+ self._write_text('cfdi_uuid', uuid)
+ self._write_text('cfdi_tipo', self.xml.attrib['tipoDeComprobante'].upper())
+ self._write_text('cfdi_folio', folio)
+ return
+
+ def _set_receptor(self, pre):
+ self._write_text('receptor_titulo')
+ receptor = self.xml.find('%sReceptor' % pre)
+ name_receptor = 'Sin nombre'
+ if 'nombre' in receptor.attrib:
+ name_receptor = receptor.attrib['nombre']
+ self._write_text('receptor_nombre', name_receptor)
+ self._write_text('receptor_rfc', receptor.attrib['rfc'])
+ self.data['receptor_rfc'] = receptor.attrib['rfc']
+ dir_fiscal = receptor.find('%sDomicilio' % pre)
+ domicilio1 = ''
+ if dir_fiscal is not None:
+ if 'calle' in dir_fiscal.attrib:
+ domicilio1 += '%s ' % dir_fiscal.attrib['calle']
+ if 'noExterior' in dir_fiscal.attrib:
+ domicilio1 += '%s ' % dir_fiscal.attrib['noExterior']
+ if 'noInterior' in dir_fiscal.attrib:
+ domicilio1 += '%s ' % dir_fiscal.attrib['noInterior']
+ domicilio2 = ''
+ if 'colonia' in dir_fiscal.attrib:
+ if dir_fiscal.attrib['colonia'].strip().lower().startswith('col.'):
+ domicilio2 += '%s, ' % dir_fiscal.attrib['colonia']
+ else:
+ domicilio2 += 'Col. %s, ' % dir_fiscal.attrib['colonia']
+ if 'codigoPostal' in dir_fiscal.attrib:
+ domicilio2 += 'C.P. %s ' % dir_fiscal.attrib['codigoPostal']
+ domicilio3 =''
+ if 'municipio' in dir_fiscal.attrib:
+ domicilio3 += '%s, ' % dir_fiscal.attrib['municipio']
+ if 'estado' in dir_fiscal.attrib:
+ domicilio3 += '%s, ' % dir_fiscal.attrib['estado']
+ if 'pais' in dir_fiscal.attrib:
+ domicilio3 += '%s ' % dir_fiscal.attrib['pais']
+ domicilio4 = ''
+ if 'localidad' in dir_fiscal.attrib:
+ domicilio4 += 'Localidad: %s, ' % dir_fiscal.attrib['localidad']
+ if 'referencia' in dir_fiscal.attrib:
+ domicilio4 += 'Referencia: %s' % dir_fiscal.attrib['referencia']
+ self._write_text('receptor_direccion1', domicilio1)
+ self._write_text('receptor_direccion2', domicilio2)
+ self._write_text('receptor_direccion3', domicilio3)
+ self._write_text('receptor_direccion4', domicilio4)
+ return
+
+ def _set_emisor(self, pre):
+ emisor = self.xml.find('%sEmisor' % pre)
+ dir_fiscal = emisor.find('%sExpedidoEn' % pre)
+ lugar = ''
+ if dir_fiscal is not None:
+ self.elements['emisor_logo']['x'] = 10
+ self.elements['emisor_nombre']['x'] = 45
+ self.elements['emisor_rfc']['x'] = 45
+ self.elements['emisor_direccion1']['x'] = 45
+ self.elements['emisor_direccion2']['x'] = 45
+ self.elements['emisor_direccion3']['x'] = 45
+ self.elements['emisor_direccion4']['x'] = 45
+ self.elements['emisor_nombre']['w'] = 75
+ self.elements['emisor_rfc']['w'] = 75
+ self.elements['emisor_direccion1']['w'] = 75
+ self.elements['emisor_direccion2']['w'] = 75
+ self.elements['emisor_direccion3']['w'] = 75
+ self.elements['emisor_direccion4']['w'] = 75
+ domicilio1 = ''
+ if 'calle' in dir_fiscal.attrib:
+ domicilio1 += '%s ' % dir_fiscal.attrib['calle']
+ if 'noExterior' in dir_fiscal.attrib:
+ domicilio1 += '%s ' % dir_fiscal.attrib['noExterior']
+ if 'noInterior' in dir_fiscal.attrib:
+ domicilio1 += '%s ' % dir_fiscal.attrib['noInterior']
+ domicilio2 = ''
+ if 'colonia' in dir_fiscal.attrib:
+ if dir_fiscal.attrib['colonia'].strip().lower().startswith('col.'):
+ domicilio2 += '%s, ' % dir_fiscal.attrib['colonia']
+ else:
+ domicilio2 += 'Col. %s, ' % dir_fiscal.attrib['colonia']
+ if 'codigoPostal' in dir_fiscal.attrib:
+ domicilio2 += 'C.P. %s ' % dir_fiscal.attrib['codigoPostal']
+ domicilio3 =''
+ if 'municipio' in dir_fiscal.attrib:
+ domicilio3 += '%s, ' % dir_fiscal.attrib['municipio']
+ lugar += '%s, ' % dir_fiscal.attrib['municipio']
+ if 'estado' in dir_fiscal.attrib:
+ domicilio3 += '%s, ' % dir_fiscal.attrib['estado']
+ lugar += '%s, ' % dir_fiscal.attrib['estado']
+ if 'pais' in dir_fiscal.attrib:
+ domicilio3 += '%s ' % dir_fiscal.attrib['pais']
+ domicilio4 = ''
+ if 'localidad' in dir_fiscal.attrib:
+ domicilio4 += '%s, ' % dir_fiscal.attrib['localidad']
+ if 'referencia' in dir_fiscal.attrib:
+ domicilio4 += '%s' % dir_fiscal.attrib['referencia']
+ self._write_text('expedido_titulo')
+ self._write_text('expedido_direccion1', domicilio1)
+ self._write_text('expedido_direccion2', domicilio2)
+ self._write_text('expedido_direccion3', domicilio3)
+ self._write_text('expedido_direccion4', domicilio4)
+
+ path_logo = 'logos/{}.png'.format(emisor.attrib['rfc'].lower())
+ self.data['emisor_rfc'] = emisor.attrib['rfc']
+ if os.path.exists(path_logo):
+ self.image(
+ path_logo,
+ self.elements['emisor_logo']['x'],
+ self.elements['emisor_logo']['y'],
+ self.elements['emisor_logo']['w'],
+ self.elements['emisor_logo']['h']
+ )
+ regimen = emisor.find('%sRegimenFiscal' % pre)
+ if regimen is None:
+ regimen = ''
+ else:
+ regimen = regimen.attrib['Regimen']
+ dir_fiscal = emisor.find('%sDomicilioFiscal' % pre)
+ lugar2 = ''
+ domicilio1 = ''
+ domicilio2 = ''
+ domicilio3 = ''
+ domicilio4 = ''
+ if not dir_fiscal is None:
+ if 'calle' in dir_fiscal.attrib:
+ domicilio1 += '%s ' % dir_fiscal.attrib['calle']
+ if 'noExterior' in dir_fiscal.attrib:
+ domicilio1 += '%s ' % dir_fiscal.attrib['noExterior']
+ if 'noInterior' in dir_fiscal.attrib:
+ domicilio1 += '%s ' % dir_fiscal.attrib['noInterior']
+
+ if 'colonia' in dir_fiscal.attrib:
+ if dir_fiscal.attrib['colonia'].strip().lower().startswith('col.'):
+ domicilio2 += '%s, ' % dir_fiscal.attrib['colonia']
+ else:
+ domicilio2 += 'Col. %s, ' % dir_fiscal.attrib['colonia']
+ if 'codigoPostal' in dir_fiscal.attrib:
+ domicilio2 += 'C.P. %s ' % dir_fiscal.attrib['codigoPostal']
+
+ if 'municipio' in dir_fiscal.attrib:
+ domicilio3 += '%s, ' % dir_fiscal.attrib['municipio']
+ lugar2 += '%s, ' % dir_fiscal.attrib['municipio']
+ if 'estado' in dir_fiscal.attrib:
+ domicilio3 += '%s, ' % dir_fiscal.attrib['estado']
+ lugar2 += '%s, ' % dir_fiscal.attrib['estado']
+ if 'pais' in dir_fiscal.attrib:
+ domicilio3 += '%s ' % dir_fiscal.attrib['pais']
+
+ if 'localidad' in dir_fiscal.attrib:
+ domicilio4 += '%s, ' % dir_fiscal.attrib['localidad']
+ if 'referencia' in dir_fiscal.attrib:
+ domicilio4 += '%s' % dir_fiscal.attrib['referencia']
+ name_emisor = 'Sin nombre'
+ if 'nombre' in emisor.attrib:
+ name_emisor = emisor.attrib['nombre']
+ self._write_text('emisor_nombre', name_emisor)
+ self._write_text('emisor_rfc', emisor.attrib['rfc'])
+ self._write_text('emisor_direccion1', domicilio1)
+ self._write_text('emisor_direccion2', domicilio2)
+ self._write_text('emisor_direccion3', domicilio3)
+ self._write_text('emisor_direccion4', domicilio4)
+ if not lugar:
+ lugar = lugar2
+ self.data['expedicion'] = lugar
+ self.data['regimen'] = regimen
+ return
+
+ def _rgb(self, col):
+ return (col // 65536), (col // 256 % 256), (col % 256)
+
+ def _color(self, r, g, b):
+ return int('%02x%02x%02x' % (r, g, b), 16)
+
+ def _write_text(self, k, s=''):
+ if not k in self.elements: return
+ self.pos_size['x'] = self.elements[k]['x']
+ self.pos_size['y'] = self.elements[k]['y']
+ self.pos_size['w'] = self.elements[k]['w']
+ self.pos_size['h'] = self.elements[k]['h']
+ self.set_font(
+ self.elements[k]['font'],
+ self.elements[k]['style'],
+ self.elements[k]['size']
+ )
+ self.set_text_color(*self._rgb(self.elements[k]['foreground']))
+ self.set_fill_color(*self._rgb(self.elements[k]['background']))
+ self.set_xy(self.pos_size['x'], self.pos_size['y'])
+ if self.elements[k]['border'] == '0' or \
+ self.elements[k]['border'] == '1':
+ border = eval(self.elements[k]['border'])
+ else:
+ border = self.elements[k]['border']
+ if not s:
+ s = self.elements[k]['text']
+ if self.elements[k]['multiline']:
+ self.multi_cell(
+ self.pos_size['w'],
+ self.pos_size['h'],
+ s,
+ border,
+ self.elements[k]['align'],
+ bool(self.elements[k]['background'])
+ )
+ else:
+ self._ajust_text(s, self.elements[k]['size'])
+ self.cell(
+ self.pos_size['w'],
+ self.pos_size['h'],
+ s,
+ border,
+ 0,
+ self.elements[k]['align'],
+ bool(self.elements[k]['background'])
+ )
+ return
+
+ def _ajust_text(self, s, size):
+ if not isinstance(s, str):
+ s = str(s)
+ s = s.encode('ascii', 'replace')
+ while True:
+ if self.get_string_width(s) < (self.pos_size['w']-0.8):
+ break
+ else:
+ size -= 1
+ self.set_font_size(size)
+ return
+
+ def _get_timbre(self, pre):
+ com = self.xml.find('%sComplemento' % pre)
+ if com is None: return {}
+ timbre = com.find('%sTimbreFiscalDigital' % self.G.PREFIX['TIMBRE'])
+ if timbre is None:
+ return {}
+ else:
+ return timbre.attrib
+
+ def _get_cadena(self):
+ return self.G.CADENA.format(**self.timbre)
+
+ def _parse_csv(self, template='', emisor_rfc=''):
+ "Parse template format csv file and create elements dict"
+ keys = ('x', 'y', 'w', 'h', 'font', 'size', 'style', 'foreground',
+ 'background', 'align', 'priority', 'multiline', 'border', 'text')
+ self.elements = {}
+ if template:
+ path_template = template
+ else:
+ path_template = '{}/{}.csv'.format(
+ self.G.PATHS['TEMPLATE'],
+ emisor_rfc.lower()
+ )
+ if not os.path.exists(path_template):
+ path_template = '{}/default.csv'.format(self.G.PATHS['TEMPLATE'])
+ reader = csv.reader(open(path_template, 'r'), delimiter=',')
+ for row in reader:
+ new_list = []
+ for v in row[1:]:
+ if v == (''):
+ new_list.append('')
+ elif v.startswith("'"):
+ new_list.append(v[1:-1])
+ else:
+ new_list.append(eval(v.strip()))
+ self.elements[row[0][1:-1]] = dict(zip(keys, new_list))
+ return
+
diff --git a/admincfdi/template/default.csv b/admincfdi/template/default.csv
new file mode 100644
index 0000000..ce81a64
--- /dev/null
+++ b/admincfdi/template/default.csv
@@ -0,0 +1,77 @@
+'headers','x','y','w','h','font','size','style','foreground','background','align','priority','multiline','border', 'text'
+'emisor_logo',30,5,35,30,'',0,'',0,0,'',0,0,''
+'emisor_nombre',85,5,100,4,'Helvetica',10,'B',6291456,14803425,'C',0,0,'0',''
+'emisor_rfc',85,9,100,4,'Helvetica',10,'B',6291456,14803425,'C',0,0,'0',''
+'emisor_direccion1',85,13,100,4,'Helvetica',9,'B',0,0,'C',0,0,'0',''
+'emisor_direccion2',85,17,100,4,'Helvetica',9,'B',0,0,'C',0,0,'0',''
+'emisor_direccion3',85,21,100,4,'Helvetica',9,'B',0,0,'C',0,0,'0',''
+'emisor_direccion4',85,25,100,4,'Helvetica',9,'B',0,0,'C',0,0,'0',''
+'expedido_titulo',130,5,75,4,'Helvetica',7,'B',6291456,14803425,'C',0,0,'0','Expedido En'
+'expedido_direccion1',130,9,75,4,'Helvetica',8,'B',0,0,'C',0,0,'0',''
+'expedido_direccion2',130,13,75,4,'Helvetica',8,'B',0,0,'C',0,0,'0',''
+'expedido_direccion3',130,17,75,4,'Helvetica',8,'B',0,0,'C',0,0,'0',''
+'expedido_direccion4',130,21,75,4,'Helvetica',8,'B',0,0,'C',0,0,'0',''
+'receptor_titulo',10,40,100,4,'Helvetica',7,'B',6291456,14803425,'C',0,0,'0','Receptor'
+'receptor_nombre',15,45,95,4,'Helvetica',10,'B',6291456,0,'L',0,0,'0',''
+'receptor_rfc',15,49,95,4,'Helvetica',10,'B',6291456,0,'L',0,0,'0',''
+'receptor_direccion1',15,53,100,4,'Helvetica',9,'B',0,0,'L',0,0,'0',''
+'receptor_direccion2',15,57,100,4,'Helvetica',9,'B',0,0,'L',0,0,'0',''
+'receptor_direccion3',15,61,100,4,'Helvetica',9,'B',0,0,'L',0,0,'0',''
+'receptor_direccion4',15,65,100,4,'Helvetica',8,'B',0,0,'L',0,0,'0',''
+'cfdi_titulo',120,40,85,4,'Helvetica',7,'B',6291456,14803425,'C',0,0,'0',''
+'cfdi_fecha',10,35,170,4,'Helvetica',8,'',0,0,'R',0,0,'0',''
+'cfdi_hora',180,35,20,4,'Helvetica',6,'B',0,0,'R',0,0,'0',''
+'cfdi_titulo1',120,44,30,4,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0','Serie CSD Emisor'
+'cfdi_titulo2',120,48,30,4,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0',''
+'cfdi_titulo3',120,52,30,4,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0',''
+'cfdi_titulo4',120,56,30,4,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0','Tipo de Comprobante'
+'cfdi_titulo5',120,60,30,4,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0','Folio'
+'cfdi_regimen',120,64,85,4,'Helvetica',7,'B',6291456,14803425,'C',0,0,'0',''
+'cfdi_csd',150,44,55,4,'Helvetica',7,'',0,0,'C',0,0,'0',''
+'cfdi_csdsat',150,48,55,4,'Helvetica',7,'',0,0,'C',0,0,'0',''
+'cfdi_uuid',150,52,55,4,'Helvetica',7,'',0,0,'C',0,0,'0',''
+'cfdi_tipo',150,56,55,4,'Helvetica',7,'',0,0,'C',0,0,'0',''
+'cfdi_folio',150,60,55,4,'Helvetica',12,'B',16711680,0,'C',0,0,'0',''
+'clave_titulo',10,70,20,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'R','Clave'
+'descripcion_titulo',30,70,85,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'R','Descripcion'
+'unidad_titulo',115,70,15,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'R','Unidad'
+'cantidad_titulo',130,70,20,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'R','Cantidad'
+'pu_titulo',150,70,25,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'R','Valor Unitario'
+'importe_titulo',175,70,30,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'','Importe'
+'clave',10,73,20,3,'Helvetica',7,'',0,0,'C',0,0,'T','Sin'
+'descripcion',30,73,85,3,'Helvetica',7,'',0,0,'J',0,1,'T',''
+'unidad',115,73,15,3,'Helvetica',7,'',0,0,'C',0,0,'T',''
+'cantidad',130,73,20,3,'Helvetica',7,'',0,0,'R',0,0,'T',''
+'pu',150,73,25,3,'Helvetica',7,'',0,0,'R',0,0,'T',''
+'importe',175,73,30,3,'Helvetica',7,'',0,0,'R',0,0,'T',''
+'subtotal_titulo',150,0,25,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Subtotal'
+'subtotal',175,0,30,4,'Helvetica',8,'B',0,0,'R',0,0,'0',''
+'motivo_titulo',10,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Motivo del descuento'
+'motivo_descuento',50,0,95,4,'Helvetica',8,'',0,0,'L',0,0,'0',''
+'imp_tras_titulo',10,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Total impuestos trasladados'
+'imp_rete_titulo',80,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Total impuestos retenidos'
+'imp_trasladado',50,0,25,4,'Helvetica',8,'B',0,0,'R',0,0,'0',''
+'imp_retenido',120,0,25,4,'Helvetica',8,'B',0,0,'R',0,0,'0',''
+'en_letras',10,0,195,4,'Helvetica',7,'B',0,0,'C',0,0,'0',''
+'otros_titulo',10,0,25,3,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0',''
+'otros',35,0,40,3,'Helvetica',7,'',0,0,'L',0,0,'0',''
+'dona_aut_titulo',10,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','No. Autorizacion'
+'dona_fecha_titulo',80,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Fecha de Autorizacion'
+'dona_aut',50,0,25,4,'Helvetica',8,'B',0,0,'L',0,0,'0',''
+'dona_fecha',120,0,25,4,'Helvetica',8,'B',0,0,'L',0,0,'0',''
+'dona_leyenda',10,0,195,4,'Helvetica',6,'',6291456,14803425,'J',0,0,'0',''
+'fiscal1_titulo',10,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Disposicion Fiscal'
+'fiscal2_titulo',80,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Norma'
+'fiscal1',50,0,25,4,'Helvetica',8,'B',0,0,'C',0,0,'0',''
+'fiscal2',120,0,85,4,'Helvetica',8,'B',0,0,'L',0,0,'0',''
+'fiscal_leyenda',10,0,195,4,'Helvetica',6,'',6291456,14803425,'J',0,1,'0',''
+'qr_cbb',10,0,30,30,'',0,'',0,0,'',0,0,''
+'sello_cfd_titulo',45,0,160,4,'Helvetica',6,'B',0,0,'L',0,0,'0','Sello Digital del CFD'
+'sello_cfd',45,0,160,4,'Helvetica',6,'',6291456,14803425,'J',0,1,'0',''
+'sello_sat_titulo',45,0,160,4,'Helvetica',6,'B',0,0,'L',0,0,'0','Sello del SAT'
+'sello_sat',45,0,160,4,'Helvetica',6,'',6291456,14803425,'J',0,1,'0',''
+'fecha_titulo',45,0,35,4,'Helvetica',6,'B',0,0,'L',0,0,'0','Fecha y hora de certificacion:'
+'fecha',80,0,50,4,'Helvetica',6,'',0,0,'L',0,0,'0',''
+'cadena_titulo',10,0,195,4,'Helvetica',6,'B',0,0,'L',0,0,'0','Cadena original del complemento de certificacion digital del SAT'
+'cadena',10,0,195,4,'Helvetica',6,'',6291456,14803425,'J',0,1,'0',''
+'leyenda',65.5,0,80,4,'Helvetica',6,'',6291456,14803425,'C',0,0,'0','Este documento es una representacion impresa de CFD'
\ No newline at end of file
diff --git a/admincfdi/template/plantilla_factura.ods b/admincfdi/template/plantilla_factura.ods
new file mode 100644
index 0000000..6f42094
Binary files /dev/null and b/admincfdi/template/plantilla_factura.ods differ
diff --git a/admincfdi/tests/test_pyutil.py b/admincfdi/tests/test_pyutil.py
new file mode 100644
index 0000000..5962e5b
--- /dev/null
+++ b/admincfdi/tests/test_pyutil.py
@@ -0,0 +1,274 @@
+import unittest
+
+
+class LeeCredenciales(unittest.TestCase):
+
+ def setUp(self):
+ from unittest.mock import Mock
+ from admincfdi import pyutil
+
+ self.onefile = Mock()
+ self.onefile.readline.return_value = 'rfc pwd'
+ pyutil.open = Mock(return_value=self.onefile)
+
+ def tearDown(self):
+ from admincfdi import pyutil
+
+ del pyutil.open
+
+ def test_file_not_found(self):
+ from admincfdi import pyutil
+
+ pyutil.open.side_effect = FileNotFoundError
+ util = pyutil.Util()
+ status, rfc, pwd = util.lee_credenciales('ruta')
+ self.assertEqual('Archivo no encontrado: ruta', status)
+
+ def test_not_two_fields(self):
+ from admincfdi import pyutil
+
+ self.onefile.readline.return_value = ''
+ util = pyutil.Util()
+ status, rfc, pwd = util.lee_credenciales('ruta')
+ self.assertEqual('No contiene dos campos: ruta', status)
+
+ def test_success(self):
+ from admincfdi import pyutil
+
+ util = pyutil.Util()
+ status, rfc, pwd = util.lee_credenciales('ruta')
+ self.assertEqual('Ok', status)
+
+ def test_surplus_whitespace_is_ok(self):
+ from admincfdi import pyutil
+
+ self.onefile.readline.return_value = ' \t rfc \t pwd \r\n'
+ util = pyutil.Util()
+ status, rfc, pwd = util.lee_credenciales('ruta')
+ self.assertEqual('rfc', rfc)
+ self.assertEqual('pwd', pwd)
+
+
+class DescargaSAT(unittest.TestCase):
+
+ def setUp(self):
+ import time
+ from unittest.mock import Mock
+ from selenium import webdriver
+ from admincfdi import pyutil
+
+ self.webdriver = pyutil.webdriver
+ pyutil.webdriver = Mock()
+ pyutil.webdriver.FirefoxProfile = webdriver.FirefoxProfile
+
+ self.WebDriverWait = pyutil.WebDriverWait
+ self.wait = Mock()
+ pyutil.WebDriverWait = Mock(return_value=self.wait)
+
+ self.sleep = time.sleep
+ time.sleep = Mock()
+
+ self.status = Mock()
+ self.transfer = Mock()
+
+ pyutil.print = Mock()
+
+ def tearDown(self):
+ import time
+ from admincfdi import pyutil
+
+ pyutil.webdriver = self.webdriver
+ pyutil.WebDriverWait = self.WebDriverWait
+ time.sleep = self.sleep
+ del pyutil.print
+
+ def test_get_firefox_profile(self):
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ descarga = DescargaSAT()
+ profile = descarga.get_firefox_profile('carpeta_destino')
+ self.assertIsInstance(profile, webdriver.FirefoxProfile)
+
+ def test_connect(self):
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ profile = descarga.connect(profile, rfc='x', ciec='y')
+ self.assertEqual(3, self.status.call_count)
+
+ def test_connect_fail(self):
+ from unittest.mock import Mock
+ from admincfdi.pyutil import DescargaSAT
+ from admincfdi.pyutil import WebDriverWait
+ from selenium import webdriver
+ from selenium.common.exceptions import TimeoutException
+
+ self.wait.until.side_effect = TimeoutException
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ profile = descarga.connect(profile, rfc='x', ciec='y')
+ self.assertRaises(TimeoutException)
+
+ def test_disconnect_not_connected(self):
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ descarga = DescargaSAT(status_callback=self.status)
+ descarga.disconnect()
+ self.assertEqual(0, self.status.call_count)
+
+ def test_disconnect(self):
+ from unittest.mock import Mock
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ descarga = DescargaSAT(status_callback=self.status)
+ descarga.browser = Mock()
+ descarga.disconnect()
+ self.assertEqual(2, self.status.call_count)
+
+ def test_search_not_connected(self):
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ results = descarga.search(uuid='uuid')
+ self.assertEqual(0, len(results))
+
+ def test_search_uuid(self):
+ from unittest.mock import MagicMock
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ descarga.browser = MagicMock()
+ results = descarga.search(uuid='uuid', día='00')
+ self.assertEqual(0, len(results))
+
+ def test_search_facturas_emitidas(self):
+ from unittest.mock import MagicMock
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ descarga.browser = MagicMock()
+ results = descarga.search(facturas_emitidas=True,
+ año=1, mes=1)
+ self.assertEqual(0, len(results))
+
+ def test_search_rfc_emisor(self):
+ from unittest.mock import MagicMock
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ descarga.browser = MagicMock()
+ results = descarga.search(rfc_emisor='x')
+ self.assertEqual(0, len(results))
+
+ def test_search_not_found(self):
+ from unittest.mock import MagicMock
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ descarga.g.SAT['found'] = 'no'
+ descarga.browser = MagicMock()
+ c = MagicMock()
+ descarga.browser.find_elements_by_class_name.return_value = [c]
+ c.get_attribute.return_value = 'x no x'
+ c.is_displayed.return_value = True
+ results = descarga.search()
+ self.assertEqual(0, len(results))
+
+ def test_search_not_found_exception(self):
+ from unittest.mock import MagicMock
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ descarga.browser = MagicMock()
+ c = MagicMock()
+ descarga.browser.find_elements_by_class_name.side_effect = Exception()
+ results = descarga.search()
+ self.assertEqual(0, len(results))
+
+ def test_search_mes_eq_día(self):
+ from unittest.mock import MagicMock
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ descarga.browser = MagicMock()
+ link = MagicMock()
+ descarga.browser.find_elements_by_link_text.return_value = [link]
+ combo = descarga.browser.find_element_by_id.return_value
+ combo.get_attribute.return_value = 'sb'
+ r = link.find_element_by_xpath.return_value
+ p = r.find_element_by_xpath.return_value
+ p.get_attribute.return_value = 'sb2'
+ results = descarga.search(día='01', mes='01')
+ self.assertEqual(0, len(results))
+
+ def test_search_minuto_final_cero(self):
+ from unittest.mock import MagicMock
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ descarga.browser = MagicMock()
+ results = descarga.search(día='01', mes='02', minuto_final='0')
+
+ expected = "document.getElementById('ctl00_MainContent_CldFecha_DdlMinutoFin').value='0';"
+ descarga.browser.execute_script.assert_any_call(expected)
+
+ def test_search_mes_completo_por_día(self):
+ from unittest.mock import MagicMock, Mock
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ descarga.browser = MagicMock()
+ descarga._download_sat_month = Mock(return_value=[])
+ results = descarga.search(día='00',
+ mes_completo_por_día=True)
+ self.assertEqual(0, len(results))
+
+ def test_search_mes_ne_día(self):
+ from unittest.mock import MagicMock
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status)
+ descarga.browser = MagicMock()
+ results = descarga.search(día='01', mes='02')
+ self.assertEqual(0, len(results))
+
+ def test_download(self):
+ from unittest.mock import MagicMock
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+ descarga = DescargaSAT(status_callback=self.status,
+ download_callback=self.transfer)
+ descarga.browser = MagicMock()
+ docs = [MagicMock()]
+ descarga.download(docs)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/ui/mainwindow.ui b/admincfdi/ui/mainwindow.ui
similarity index 99%
rename from ui/mainwindow.ui
rename to admincfdi/ui/mainwindow.ui
index 744a58f..1de975a 100644
--- a/ui/mainwindow.ui
+++ b/admincfdi/ui/mainwindow.ui
@@ -1688,9 +1688,9 @@
-
-
+ {Sans} 10 {}#ff5500readonly
- string:template_json
+ string:template_csv36
@@ -1787,8 +1787,8 @@
-
- button_select_template_json_click
+
+ button_select_template_csv_clickcsv.gif1
diff --git a/values.py b/admincfdi/values.py
similarity index 91%
rename from values.py
rename to admincfdi/values.py
index 28adbe5..2d5d695 100644
--- a/values.py
+++ b/admincfdi/values.py
@@ -23,7 +23,7 @@ class Global(object):
OS = sys.platform
MAIN = 'mainwindow'
TITLE = 'Admin CFDI - Factura Libre'
- CWD = os.getcwd()
+ CWD = os.path.dirname(__file__)
PATHS = {
'current': CWD,
'img': os.path.join(CWD, 'img'),
@@ -37,6 +37,11 @@ class Global(object):
'XSLT_SELLO_SAT': os.path.join(CWD, 'bin', 'get_sello_sat.xslt'),
'XSLT_CADENA': os.path.join(CWD, 'bin', 'cfdi_{}.xslt'),
'XSLT_TIMBRE': os.path.join(CWD, 'bin', 'timbre_1.0.xslt'),
+ 'TEMPLATE': os.path.join(CWD, 'template'),
+ 'USER': os.path.expanduser('~')
+ }
+ SCRIPTS = {
+ 'CFDI2PDF': os.path.abspath(os.path.join(CWD, '..', 'cfdi2pdf')),
}
EXT_XML = '.xml'
EXT_ODS = '.ods'
@@ -53,7 +58,7 @@ class Global(object):
FILES = {
'main': os.path.join(PATHS['ui'], 'mainwindow.ui'),
'config': os.path.join(PATHS['ui'], 'config.ini'),
- 'log': os.path.join(CWD, 'admincfdi.log'),
+ 'log': os.path.join(PATHS['USER'], 'admincfdi.log'),
}
FILE_NAME = '{serie}{folio:06d}_{fecha}_{receptor_rfc}'
CADENA = '||{version}|{UUID}|{FechaTimbrado}|{selloCFD}|{noCertificadoSAT}||'
@@ -147,6 +152,7 @@ class Global(object):
'IMP_LOCAL': '{http://www.sat.gob.mx/implocal}',
'IEDU': '{http://www.sat.gob.mx/iedu}',
'DONATARIA': '{http://www.sat.gob.mx/donat}',
+ 'LEYENDAS': '{http://www.sat.gob.mx/leyendasFiscales}',
}
page_init = 'https://cfdiau.sat.gob.mx/nidp/app/login?id=SATUPCFDiCon&' \
'sid=0&option=credential&sid=0'
@@ -165,18 +171,23 @@ class Global(object):
'year': 'DdlAnio',
'month': 'ctl00_MainContent_CldFecha_DdlMes',
'day': 'ctl00_MainContent_CldFecha_DdlDia',
+ 'hour': 'ctl00_MainContent_CldFechaFinal2_DdlHora',
+ 'minute': 'ctl00_MainContent_CldFechaFinal2_DdlMinuto',
+ 'second': 'ctl00_MainContent_CldFechaFinal2_DdlSegundo',
'submit': 'ctl00_MainContent_BtnBusqueda',
'download': 'BtnDescarga',
'emisor': 'ctl00_MainContent_TxtRfcReceptor',
'receptor': 'ctl00_MainContent_TxtRfcReceptor',
'uuid': 'ctl00_MainContent_TxtUUID',
- 'combos': 'sbToggle',
+ 'combos': 'sbToggle_{}',
'found': 'No existen registros que cumplan con los criterios de',
'subtitle': 'subtitle',
'page_init': page_init,
'page_cfdi': page_cfdi,
'page_receptor': page_cfdi.format('ConsultaReceptor.aspx'),
'page_emisor': page_cfdi.format('ConsultaEmisor.aspx'),
+ 'resultados': 'ctl00_MainContent_PnlResultados',
+ 'noresultados': 'ctl00_MainContent_PnlNoResultados',
'start_hour': 'ctl00_MainContent_CldFecha_DdlHora',
'start_minute': 'ctl00_MainContent_CldFecha_DdlMinuto',
'start_second': 'ctl00_MainContent_CldFecha_DdlSegundo',
diff --git a/cfdi2pdf b/cfdi2pdf
new file mode 100755
index 0000000..1ccbea9
--- /dev/null
+++ b/cfdi2pdf
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+import argparse
+import sys
+
+from admincfdi.pyutil import CSVPDF
+from admincfdi.pyutil import Util
+
+def process_command_line_arguments():
+ parser = argparse.ArgumentParser(
+ description='Crea un PDF desde una plantilla CSV')
+
+ help = 'Archivo XML origen.'
+ parser.add_argument('-a', '--archivo-xml', help=help, default='')
+
+ help = 'Directorio origen.'
+ parser.add_argument('-o', '--directorio-origen', help=help, default='')
+
+ help = 'Directorio destino.'
+ parser.add_argument('-d', '--directorio-destino', help=help, default='')
+
+ help = 'Plantilla CSV.'
+ parser.add_argument('-p', '--plantilla', help=help, default='')
+
+ args = parser.parse_args()
+ return args
+
+
+def main():
+ ext_pdf = '.pdf'
+ args = process_command_line_arguments()
+ util = Util()
+
+ if args.archivo_xml and util.exists(args.archivo_xml):
+ if args.directorio_destino:
+ info = util.get_path_info(args.archivo_xml)
+ path_pdf = util.join(args.directorio_destino, info[0][len(args.directorio_origen)+1:])
+ util.makedirs(path_pdf)
+ path_pdf = util.join(path_pdf, info[2] + ext_pdf)
+ else:
+ path_pdf = util.replace_extension(args.archivo_xml, ext_pdf)
+ pdf = CSVPDF(args.archivo_xml, args.plantilla)
+ if pdf.xml:
+ pdf.make_pdf()
+ pdf.output(path_pdf, 'F')
+ sys.exit(0)
+ if args.directorio_origen:
+ files = util.get_files(args.directorio_origen)
+ for f in files:
+ path_pdf = util.replace_extension(f, ext_pdf)
+ pdf = CSVPDF(f, args.plantilla)
+ if pdf.xml:
+ pdf.make_pdf()
+ pdf.output(path_pdf, 'F')
+
+if __name__ == '__main__':
+ main()
diff --git a/cfdi2pdf.cmd b/cfdi2pdf.cmd
new file mode 100644
index 0000000..ce6bcfe
--- /dev/null
+++ b/cfdi2pdf.cmd
@@ -0,0 +1,4 @@
+@echo off
+set PYTHONIOENCODING=utf-8
+set c2p_path="%~d0\%~p0cfdi2pdf"
+py %c2p_path% %*
diff --git a/contributors.txt b/contributors.txt
new file mode 100644
index 0000000..10033b5
--- /dev/null
+++ b/contributors.txt
@@ -0,0 +1,5 @@
+Mauricio Baeza
+Miguel Trujillo
+Patricio Paez
+Sergio Gutierrez
+Jonathan Lopez
diff --git a/descarga-cfdi b/descarga-cfdi
new file mode 100755
index 0000000..5b8eabe
--- /dev/null
+++ b/descarga-cfdi
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+#! coding: utf-8
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 3, 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 MERCHANTIBILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+
+
+import argparse
+import time
+import datetime
+import os
+import getpass
+import sys
+
+from admincfdi.pyutil import DescargaSAT
+from admincfdi.pyutil import Util
+
+
+def process_command_line_arguments():
+ parser = argparse.ArgumentParser(description='Descarga CFDIs del SAT a una carpeta local')
+
+ default_archivo_credenciales = 'credenciales.conf'
+ help = 'Archivo con credenciales para el SAT. ' \
+ 'RFC y CIEC en el primer renglón y ' \
+ 'separadas por un espacio. ' \
+ 'El predeterminado es %(default)s'
+ parser.add_argument('--archivo-de-credenciales',
+ help=help, default=default_archivo_credenciales)
+
+ help = 'Solicitar credenciales para el SAT al inicio. '
+ parser.add_argument('--solicitar-credenciales',
+ action='store_const', const=True,
+ help=help, default=False)
+
+ home_path = os.environ.get('HOME')
+ if not home_path:
+ home_path = os.environ.get('HOMEPATH', '')
+ default_carpeta_destino = os.path.join(
+ home_path, 'cfdi-descarga')
+ help = 'Carpeta local para guardar los CFDIs descargados ' \
+ 'El predeterminado es %(default)s'
+ parser.add_argument('--carpeta-destino',
+ help=help, default=default_carpeta_destino)
+
+ help = 'Descargar facturas emitidas. ' \
+ 'Por omisión se descargan facturas recibidas'
+ parser.add_argument('--facturas-emitidas',
+ action='store_const', const=True,
+ help=help, default=False)
+
+ help = 'UUID. Por omisión no se usa en la búsqueda. ' \
+ 'Esta opción tiene precedencia sobre las demás ' \
+ 'opciones de búsqueda.'
+ parser.add_argument('--uuid',
+ help=help, default='')
+
+ help = 'RFC del emisor. Por omisión no se usa en la búsqueda.'
+ parser.add_argument('--rfc-emisor',
+ help=help, default='')
+
+ today = datetime.date.today()
+ help = 'Año. El valor por omisión es el año en curso'
+ parser.add_argument('--año',
+ help=help, default=str(today.year))
+
+ help = 'Mes. El valor por omisión es el mes en curso'
+ parser.add_argument('--mes',
+ help=help, default='{:02d}'.format(today.month))
+
+ help = 'Día. Por omisión no se usa en la búsqueda.'
+ parser.add_argument('--día',
+ help=help, default='00')
+
+ help = 'Hora inicial. Por omisión es 0.'
+ parser.add_argument('--hora-inicial',
+ help=help, default='0')
+
+ help = 'Minuto inicial. Por omisión es 0.'
+ parser.add_argument('--minuto-inicial',
+ help=help, default='0')
+
+ help = 'Segundo inicial. Por omisión es 0.'
+ parser.add_argument('--segundo-inicial',
+ help=help, default='0')
+
+ help = 'Hora final. Por omisión es 23.'
+ parser.add_argument('--hora-final',
+ help=help, default='23')
+
+ help = 'Minuto final. Por omisión es 59.'
+ parser.add_argument('--minuto-final',
+ help=help, default='59')
+
+ help = 'Segundo final. Por omisión es 59.'
+ parser.add_argument('--segundo-final',
+ help=help, default='59')
+
+ help = 'Mes completo por día. Por omisión no se usa en la búsqueda.'
+ parser.add_argument('--mes-completo-por-día', action='store_const', const=True,
+ help=help, default=False)
+
+ args=parser.parse_args()
+ return args
+
+def main():
+
+ args = process_command_line_arguments()
+ if args.solicitar_credenciales:
+ rfc = input('RFC: ')
+ pwd = getpass.getpass('CIEC: ')
+ else:
+ util = Util()
+ status, rfc, pwd = util.lee_credenciales(args.archivo_de_credenciales)
+ if status != 'Ok':
+ print(status)
+ sys.exit(1)
+ descarga = DescargaSAT()
+ profile = descarga.get_firefox_profile(args.carpeta_destino)
+ try:
+ descarga.connect(profile, rfc=rfc, ciec=pwd)
+ docs = descarga.search(facturas_emitidas= args.facturas_emitidas,
+ uuid=args.uuid,
+ rfc_emisor=args.rfc_emisor,
+ año=args.año,
+ mes=args.mes,
+ día=args.día,
+ hora_inicial=args.hora_inicial,
+ minuto_inicial=args.minuto_inicial,
+ segundo_inicial=args.segundo_inicial,
+ hora_final=args.hora_final,
+ minuto_final=args.minuto_final,
+ segundo_final=args.segundo_final,
+ mes_completo_por_día=args.mes_completo_por_día)
+ descarga.download(docs)
+ except Exception as e:
+ print (e)
+ finally:
+ descarga.disconnect()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/descarga-cfdi.cmd b/descarga-cfdi.cmd
new file mode 100755
index 0000000..7cdb918
--- /dev/null
+++ b/descarga-cfdi.cmd
@@ -0,0 +1,4 @@
+@echo off
+set PYTHONIOENCODING=utf-8
+set dc_path="%~d0\%~p0descarga-cfdi"
+py %dc_path% %*
diff --git a/docs/conf.py b/docs/conf.py
index 685c8b7..ff9065b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -19,7 +19,7 @@
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.insert(0, os.path.abspath('.'))
+sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ------------------------------------------------
@@ -29,7 +29,7 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
-extensions = []
+extensions = ['sphinx.ext.autodoc']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -45,7 +45,7 @@
# General information about the project.
project = 'admin-cfdi'
-copyright = '2015, Mauricio Baeza'
+copyright = '2015, Python Cabal'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -111,7 +111,7 @@
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
-html_title = "Documentación de {} v{}".format(project, release)
+html_title = "Documentacion de {} v{}".format(project, release)
# A shorter title for the navigation bar. Default is the same as html_title.
html_short_title = html_title
diff --git a/docs/devel.rst b/docs/devel.rst
index d01bdf1..c2c89fb 100644
--- a/docs/devel.rst
+++ b/docs/devel.rst
@@ -1,3 +1,325 @@
==========
Desarrollo
==========
+Este capítulo contiene información útil para quienes desean
+desarrollar aplicaciones que trabajen con CFDIs, para lo
+cual pueden usar una o más de las clases disponibles dentro
+del paquete `admincfdi`.
+
+
+Estructura
+==========
+
+El paquete `admincfdi` incluye los siguientes módulos:
+
+- `pyutil` Tiene varias clases que implementan
+ las funcionalidades usadas por las aplicaciones.
+
+- `values` Tiene la clase Global que centraliza
+ valores que se usan en los otros módulos. Por
+ ejemplo, las URLs y valores id de la página web
+ de CFDIs del SAT están en el atributo SAT,
+ es un diccionario que es usado
+ en la descarga de CFDIs.
+
+admin-cfdi
+==========
+
+El botón ``Descargar`` está ligado al método
+:func:`admin-cfdi.Application.button_download_sat_click`
+de la aplicación, que ejecuta
+estos dos métodos:
+
+- :func:`admin-cfdi.Application._validate_download_sat`
+
+- :func:`admin-cfdi.Application._download_sat`
+
+Descarga de facturas del SAT
+============================
+
+El proceso de descarga mediante la aplicación de CFDIs
+del SAT consiste en estos pasos:
+
+#. Conectar
+#. Buscar
+#. Descargar
+#. Desconectar
+
+Los detalles de cada paso:
+
+#. Conectar
+
+ - Lanzar el navegador
+ - Navegar a la página de login de CFDIs
+ - Llenar el usuario y la contraseña (RFC y CIEC)
+ - Enviar los datos al servidor
+ - Esperar la respuesta
+ - En caso de éxito, se carga una página con el título
+ *NetIQ Access Manager*
+ - En caso de falla, un elemento ``div`` con id ``xacerror``
+ deja de estar oculto y muestra su texto que empieza
+ con *El RFC o contraseña son incorrectos.*
+
+#. Buscar
+
+ - Navegar a la página de búsqueda de facturas emitidas,
+ o a la de facturas recibidas
+ - Esperar a que el título cambie a *Buscar CFDI*
+ - Llenar los datos de la búsqueda
+ - Si la búsqueda es por UUID, llenar el UUID en
+ el input con id ``ctl00_MainContent_TxtUUID``.
+ - Si la búsqueda es por fecha:
+ - Hacer clic en el botón de radio a la izquierda
+ de Fecha de Emisión con id
+ ``ctl00_MainContent_RdoFechas``.
+ - Esperar a que el input a la derecha de RFC Emisor
+ con id ``ctl00_MainContent_TxtRfcReceptor``
+ esté habilitado y se pueda hacer clic en él.
+ - Si se buscan facturas emitidas:
+ - Habilitar los inputs con id
+
+ - ``ctl00_MainContent_CldFechaInicial2_Calendario_text``
+ - ``ctl00_MainContent_CldFechaFinal2_Calendario_text``
+
+ y asignar valor de fecha inicial y fecha final de emisión
+ usando formato ``dd/mm/aaaa``
+ - Asignar a los selects no visibles de tiempo final con ids
+
+ - ``ctl00_MainContent_CldFechaFinal2_DdlHora``
+ - ``ctl00_MainContent_CldFechaFinal2_DdlMinuto``
+ - ``ctl00_MainContent_CldFechaFinal2_DdlSegundo``
+
+ una cadena con un valor en el rango que corresponde
+ respectivamente: 1 a 23, 1 a 59 y 1 a 59.
+ No usar ceros a la izquierda para valores menores
+ a 10: usar 5, no 05. Por omisión hay que llenar cada
+ select con el valor máximo correspondiente.
+ - Se se buscan facturas recibidas:
+ - Asignar a los selects no visibles con ids
+
+ - ``DdlAnio``
+ - ``ctl00_MainContent_CldFecha_DdlMes``
+ - ``ctl00_MainContent_CldFecha_DdlDia``
+ - ``ctl00_MainContent_CldFecha_DdlHora``
+ - ``ctl00_MainContent_CldFecha_DdlMinuto``
+ - ``ctl00_MainContent_CldFecha_DdlSegundo``
+ - ``ctl00_MainContent_CldFecha_DdlHoraFin``
+ - ``ctl00_MainContent_CldFecha_DdlMinutoFin``
+ - ``ctl00_MainContent_CldFecha_DdlSegundoFin``
+
+ los valores de los parámetros año, mes, día,
+ hora_inicial, minuto_inicial, segundo_inicial,
+ hora_final, minuto_final y segundo_final respectivamente.
+ Los valores de horas, minutos y segundos
+ no deben llevar 0 a la izquierda. El valor de
+ día requiere 0 a la izquierda para valores menores
+ a 10.
+
+ - Enviar la forma de búsqueda al servidor mediante método POST, los
+ datos que se envían se muestran más bajo.
+
+ - Esperar a que no sea visible el elemento div de los
+ resultados, o el botón mismo de enviar
+ - Esperar a que uno de los dos div con id
+ ``ctl00_MainContent_PnlResultados`` o id
+ ``ctl00_MainContent_PnlNoResultados`` esté
+ visible.
+ - Si el div con id ``ctl00_MainContent_PnlResultados``
+ es visible:
+
+ - Esperar que un elemento con name ``BtnDescarga``
+ se le pueda hacer clic
+ - Encontrar la lista todos los elementos con name
+ ``BtnDescarga``. Son los íconos
+ de descarga a la izquierda en cada renglón.
+
+ - La lista de resultados está paginada en 500 elementos.
+ Si los
+ resultados son más de 500, una opción es dividir
+ la búsqueda en dos o más búsquedas
+ en las que se agregan criterios: La búsqueda de un
+ mes se puede dividir en búsquedas por día; la
+ búsqueda de un día puede dividirse en búsquedas en
+ un rango de horas en ese día.
+
+
+
+#. Descargar
+
+ - Iterar en cada elemento de la lista
+ de resultados:
+
+ - Concatenar la URL base
+ de CFDIs con el valor del atributo ``onclick``
+ del elemento
+ - Hacer la solicitud GET a esta URL
+
+#. Desconectar
+ - Cerrar la sesión
+ - Cerrar el navegador. Este paso se realiza
+ a pesar de que ocurra una falla en el paso
+ anterior.
+
+En caso de alguna falla en los primeros tres pasos,
+la aplicación debe realizar el paso 4.
+
+Los datos que se envían por la forma de búsqueda de facturas recibidas:
+
+ - ctl00$ScriptManager1=ctl00$MainContent$UpnlBusqueda|ctl00$MainContent$BtnBusqueda
+ - __CSRFTOKEN=%2FwEFJGNjZmIzNzZmLTE4OWUtNDQwNS1iNmZiLWU2NWE4MDQ0Y2EwZA%3D%3D
+ - ctl00$MainContent$TxtUUID=
+ - ctl00$MainContent$FiltroCentral=RdoFechas
+ - ctl00$MainContent$CldFecha$DdlAnio=2014
+ - ctl00$MainContent$CldFecha$DdlMes=1
+ - ctl00$MainContent$CldFecha$DdlDia=0
+ - ctl00$MainContent$CldFecha$DdlHora=0
+ - ctl00$MainContent$CldFecha$DdlMinuto=0
+ - ctl00$MainContent$CldFecha$DdlSegundo=0
+ - ctl00$MainContent$CldFecha$DdlHoraFin=23
+ - ctl00$MainContent$CldFecha$DdlMinutoFin=59
+ - ctl00$MainContent$CldFecha$DdlSegundoFin=59
+ - ctl00$MainContent$TxtRfcReceptor=
+ - ctl00$MainContent$DdlEstadoComprobante=-1
+ - ctl00$MainContent$hfInicialBool=false
+ - ctl00$MainContent$ddlComplementos=-1
+ - __EVENTTARGET=
+ - __EVENTARGUMENT=
+ - __LASTFOCUS=
+ - __VIEWSTATE=
+ - __VIEWSTATEGENERATOR=FE9DB3F4
+ - __VIEWSTATEENCRYPTED=
+ - __ASYNCPOST=true
+ - ctl00$MainContent$BtnBusqueda=Buscar CFDI
+
+Los datos que se envían por la forma de búsqueda de facturas emitidas:
+
+ - ctl00$ScriptManager1=ctl00$MainContent$UpnlBusqueda|ctl00$MainContent$BtnBusqueda
+ - __CSRFTOKEN=%2FwEFJGNjZmIzNzZmLTE4OWUtNDQwNS1iNmZiLWU2NWE4MDQ0Y2EwZA%3D%3D
+ - ctl00$MainContent$TxtUUID=
+ - ctl00$MainContent$FiltroCentral=RdoFechas
+ - ctl00$MainContent$hfInicial=2015
+ - ctl00$MainContent$hfInicialBool=false
+ - ctl00$MainContent$CldFechaInicial2$Calendario_text=18/05/2015
+ - ctl00$MainContent$CldFechaInicial2$DdlHora=0
+ - ctl00$MainContent$CldFechaInicial2$DdlMinuto=0
+ - ctl00$MainContent$CldFechaInicial2$DdlSegundo=0
+ - ctl00$MainContent$hfFinal=2015
+ - ctl00$MainContent$CldFechaFinal2$Calendario_text=19/05/2015
+ - ctl00$MainContent$CldFechaFinal2$DdlHora=23
+ - ctl00$MainContent$CldFechaFinal2$DdlMinuto=59
+ - ctl00$MainContent$CldFechaFinal2$DdlSegundo=59
+ - ctl00$MainContent$TxtRfcReceptor=
+ - ctl00$MainContent$DdlEstadoComprobante=-1
+ - ctl00$MainContent$ddlComplementos=-1
+ - ctl00$MainContent$hfDatos=
+ - ctl00$MainContent$hfFlag=
+ - ctl00$MainContent$hfAux=
+ - __EVENTTARGET=
+ - __EVENTARGUMENT=
+ - __LASTFOCUS=
+ - __VIEWSTATE=
+ - __VIEWSTATEGENERATOR=3D1378D8
+ - __VIEWSTATEENCRYPTED=
+ - __ASYNCPOST=true
+ - ctl00$MainContent$BtnBusqueda=Buscar CFDI
+
+
+
+API
+===
+El módulo :mod:`admincfdi.pyutil` provee varias clases, las cuales
+pueden ser usadas por las aplicaciones. En las siguientes
+secciones se explican y dan ejemplos de uso cada una de estas clases.
+
+
+SAT
+---
+
+ValidCFDI
+---------
+
+Util
+----
+
+Mail
+----
+
+LibO
+----
+
+NumerosLetras
+-------------
+
+CFDIPDF
+-------
+
+DescargaSAT
+-----------
+Lleva a cabo al descarga de CFDIs del sitio del SAT. Para descargar
+un conjunto de CFDIs con ciertos criterios de búsqueda, se
+utilizan los siguientes pasos:
+
+#. Instanciar :class:`~admincfdi.pyutil.DescargaSAT`::
+
+ descarga = DescargaSAT()
+
+#. Crear un perfil de Firefox::
+
+ profile = descarga.get_firefox_profile(carpeta_destino)
+
+#. Conectar al sitio del SAT, lanzando Firefox::
+
+ descarga.connect(profile, rfc=rfc, ciec=pwd)
+
+#. Realizar una búsqueda, guardando la lista de resultados
+ obtenida::
+
+ docs = descarga.search(facturas_emitidas=facturas_emitidas,
+ uuid=uuid,
+ rfc_emisor=rfc_emisor,
+ año=año,
+ mes=mes,
+ día=día,
+ mes_completo_por_día=mes_completo_por_día)
+
+#. Descargar los CFDIs::
+
+ descarga.download(docs)
+
+#. Desconectar la sesión del sitio del SAT y terminar
+ Firefox::
+
+ descarga.disconnect()
+
+Los pasos 4. de búsqueda y 5. de descarga pueden repetirse, si
+se desean descargar dos o más conjuntos de CFDIs con diferentes
+criterios de búsqueda, manteniendo la sesión original abierta.
+
+Como ejemplo, a continuación se muestra el uso de los
+pasos en las aplicaciones ``admin-cfdi`` y ``descarga-cfdi``
+que son parte del proyecto::
+
+ descarga = DescargaSAT()
+ profile = descarga.get_firefox_profile(args.carpeta_destino)
+ try:
+ descarga.connect(profile, rfc=rfc, ciec=pwd)
+ docs = descarga.search(facturas_emitidas= args.facturas_emitidas,
+ uuid=args.uuid,
+ rfc_emisor=args.rfc_emisor,
+ año=args.año,
+ mes=args.mes,
+ día=args.día,
+ mes_completo_por_día=args.mes_completo_por_día)
+ descarga.download(docs)
+ except Exception as e:
+ print (e)
+ finally:
+ descarga.disconnect()
+
+Las cláusulas ``try/except/finally`` son para manejar alguna
+excepción que ocurra en cualquiera de los pasos, y garantizar
+que en cualquier caso se hace la desconexión de la sesión
+y se termina Firefox.
+
+CSVPDF
+------
diff --git a/docs/glosario.rst b/docs/glosario.rst
index 495a527..8bb6c80 100644
--- a/docs/glosario.rst
+++ b/docs/glosario.rst
@@ -8,7 +8,7 @@ Glosario
Un administrador de documentos CFDI
CFDI
- Certificado de Firma Digital Integrada
+ Comprobante Fiscal Digital por Internet
SAT
Servicio de Administración Tributaria
diff --git a/docs/img/admin-cfdi-ventana-ppal.png b/docs/img/admin-cfdi-ventana-ppal.png
new file mode 100644
index 0000000..704128b
Binary files /dev/null and b/docs/img/admin-cfdi-ventana-ppal.png differ
diff --git a/docs/img/ejemplo-pdf.png b/docs/img/ejemplo-pdf.png
new file mode 100644
index 0000000..52f49da
Binary files /dev/null and b/docs/img/ejemplo-pdf.png differ
diff --git a/docs/index.rst b/docs/index.rst
index e7f8d95..89c368d 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -12,8 +12,10 @@ Contents:
:maxdepth: 2
intro
+ install
uso
devel
+ reference
glosario
diff --git a/docs/install.rst b/docs/install.rst
new file mode 100644
index 0000000..d4de5c1
--- /dev/null
+++ b/docs/install.rst
@@ -0,0 +1,66 @@
+===========
+Instalación
+===========
+
+Para instalar :term:`admin-cfdi` descarga la ultima versión de producción desde
+`Github`_ e instala con el comando.
+
+::
+
+ sudo python setup.py install
+
+Si lo prefieres usa un entorno virtual.
+
+#. Para `LinuxMint`_
+
+ * Crea el entorno virtual
+
+ ::
+
+ pyvenv-3.4 test_admin --without-pip
+
+ * Activalo
+
+ ::
+
+ cd test_admin/
+ source bin/activate
+
+ * Instala pip
+
+ ::
+
+ wget https://bootstrap.pypa.io/get-pip.py
+ python get-pip.py
+
+ * Instala :term:`admin-cfdi`
+
+ ::
+
+ python setup.py install
+
+#. Para `ArchLinux`_
+
+ * Crea el entorno virtual
+
+ ::
+
+ pyvenv test_admin
+
+ * Activalo
+
+ ::
+
+ cd test_admin/
+ source bin/activate
+
+ * Instala :term:`admin-cfdi`
+
+ ::
+
+ python setup.py install
+
+
+.. _Github: https://github.com/LinuxCabal/admin-cfdi
+.. _LinuxMint: http://linuxmint.com/
+.. _ArchLinux: https://www.archlinux.org/
diff --git a/docs/intro.rst b/docs/intro.rst
index b0d6ce9..5e82383 100644
--- a/docs/intro.rst
+++ b/docs/intro.rst
@@ -2,6 +2,79 @@
Introducción
============
-:term:`admin-cfdi` es una aplicación para descargar
-y administrar documentos :term:`CFDI` emitidos por el
-:term:`SAT`.
+:term:`admin-cfdi` es una aplicación de escritorio desarrollada en `Python`_ para descargar
+documentos :term:`CFDI` (facturas electrónicas) directamente del :term:`SAT`,
+permite también, descargar CFDIs desde correos electrónicos, validarlos y administrarlos.
+
+.. _Python: http://python.org/
+
+.. image:: img/admin-cfdi-ventana-ppal.png
+ :align: center
+ :width: 714
+ :height: 762
+ :scale: 50 %
+ :alt: Haz clic para agrandar
+
+`admin-cfdi` es `software libre`_ bajo la licencia `GNU GPL 3`_ y corre en GNU/Linux y
+otros sistemas operativos.
+Se tienen también dos aplicaciones de línea de comando que permiten
+automatizar operaciones mediante un script:
+
+.. _software libre: https://www.gnu.org/philosophy/free-sw.es.html
+.. _GNU GPL 3: https://www.gnu.org/licenses/gpl.html
+
+``descarga-cfdi`` descarga CFDIs que cumplan determinado criterio, por
+ejemplo todos los CFDIs recibidos en el mes de enero de 2015::
+
+ descarga-cfdi --año 2015 --mes 01
+
+ Abriendo Firefox...
+ Conectando...
+ Conectado...
+ Buscando...
+ Factura 1 de 13
+ Factura 2 de 13
+ Factura 3 de 13
+ Factura 4 de 13
+ Factura 5 de 13
+ Factura 6 de 13
+ Factura 7 de 13
+ Factura 8 de 13
+ Factura 9 de 13
+ Factura 10 de 13
+ Factura 11 de 13
+ Factura 12 de 13
+ Factura 13 de 13
+ Desconectando...
+ Desconectado..
+
+Los CFDIs se guardan por omisión en la carpeta `cfdi-descarga`::
+
+ 12FB2D4B-CAE0-41CF-B344-13FE5135C773.xml 5A5108B2-2171-49B0-86D4-539DD205786A.xml CB969AF4-0E13-441B-9CC7-0AA11831317F.xml
+ 1FBFA93D-F171-0B0E-CF71-4216C214E66F.xml 61F50926-7C47-4269-B612-3777881050A4.xml F1ABE4CE-9444-4F77-A3E5-57A6559F6CB3.xml
+ 2968F314-90D6-4000-BBA5-E17988F2870F.xml 79FE35B0-636E-4163-8BA2-38E053E97E4C.xml FF31423C-E1BC-4A3D-9A7B-472FFE9988F9.xml
+ 2CF33F44-2E2A-4F4C-904C-6213D3E8F12C.xml
+
+``cfdi2pdf`` convierte los CFDIs de una carpeta origen a formato PDF::
+
+ cfdi2pdf -o cfdi-descarga/ -d cfdi-pdf/
+
+ Generando: ../cfdi-descarga/2CF33F44-2E2A-4F4C-904C-6213D3E8F12C.xml
+ Generando: ../cfdi-descarga/79FE35B0-636E-4163-8BA2-38E053E97E4C.xml
+ Generando: ../cfdi-descarga/61F50926-7C47-4269-B612-3777881050A4.xml
+ Generando: ../cfdi-descarga/1FBFA93D-F171-0B0E-CF71-4216C214E66F.xml
+ Generando: ../cfdi-descarga/F1ABE4CE-9444-4F77-A3E5-57A6559F6CB3.xml
+ Generando: ../cfdi-descarga/2968F314-90D6-4000-BBA5-E17988F2870F.xml
+ Generando: ../cfdi-descarga/FF31423C-E1BC-4A3D-9A7B-472FFE9988F9.xml
+ Generando: ../cfdi-descarga/CB969AF4-0E13-441B-9CC7-0AA11831317F.xml
+ Generando: ../cfdi-descarga/5A5108B2-2171-49B0-86D4-539DD205786A.xml
+ Generando: ../cfdi-descarga/12FB2D4B-CAE0-41CF-B344-13FE5135C773.xml
+
+Un ejemplo de los archivos PDF generados:
+
+.. image:: img/ejemplo-pdf.png
+ :align: center
+ :width: 1366
+ :height: 768
+ :scale: 50 %
+ :alt: Haz clic para agrandar
diff --git a/docs/reference.rst b/docs/reference.rst
new file mode 100644
index 0000000..7ff63dc
--- /dev/null
+++ b/docs/reference.rst
@@ -0,0 +1,27 @@
+==========
+Referencia
+==========
+
+admincfdi
+---------
+
+.. automodule:: admincfdi
+ :members:
+ :undoc-members:
+ :private-members:
+
+pyutil
+------
+
+.. automodule:: admincfdi.pyutil
+ :members:
+ :undoc-members:
+ :private-members:
+
+values
+------
+
+.. automodule:: admincfdi.values
+ :members:
+ :undoc-members:
+ :private-members:
diff --git a/docs/uso.rst b/docs/uso.rst
index 93e46dc..cb7e177 100644
--- a/docs/uso.rst
+++ b/docs/uso.rst
@@ -1,3 +1,131 @@
===
Uso
===
+
+Aplicaciones
+------------
+Admincfdi incluye las siguientes aplicaciones:
+
+- `admin-cfdi`
+
+- `descarga-cfdi`
+
+- `cfdi2pdf`
+
+`admin-cfdi` es una aplicación gráfica, `descarga-cfdi`
+y `cfdi2pdf` son aplicaciones de línea de comando.
+
+admin-cfdi
+==========
+
+La descarga de los archivos XML del sitio web del SAT se
+maneja en la primera pestaña de la interfase gráfica.
+
+Primeramente el usuario debe llenar
+datos y/o seleccionar opciones en estos tres apartados:
+
+- Datos de acceso
+- Tipo de consulta
+- Opciones de búsqueda
+
+El proceso de la descarga se inicia mediante el botón
+``Descargar``.
+
+descarga-cfdi
+=============
+
+El avance del proceso se indica al usuario mediante
+textos cortos que se muestran en una línea de estado
+de la interfase gráfica, en esta secuencia::
+
+ Abriendo Firefox...
+ Conectando...
+ Conectado...
+ Buscando...
+ Factura 1 de 12
+ Factura 2 de 12
+ Factura 3 de 12
+ Factura 4 de 12
+ Factura 5 de 12
+ Factura 6 de 12
+ Factura 7 de 12
+ Factura 8 de 12
+ Factura 9 de 12
+ Factura 10 de 12
+ Factura 11 de 12
+ Factura 12 de 12
+ Desconectando...
+ Desconectado...
+
+
+Pruebas funcionales de descarga del SAT
+---------------------------------------
+Estas pruebas sirven para varios propósitos:
+
+- Saber si el sitio del SAT esta funcionando
+ normalmente,
+
+- Saber si nuestra conexión entre la PC
+ y el sito del SAT está funcionando y si
+ su desempeño es el esperado,
+
+- Saber si el sitio del SAT cambió su
+ funcionamiento del tal forma que sea
+ necesario actualizar la librería de
+ descarga de admincfdi.
+
+Las pruebas realizan descargas mediante
+varios modos de búsqueda y validan
+que la cantidad de archivos descargados
+sea la esperada. No requieren interacción
+mientas corren.
+
+Es necesario crear un archivo de credenciales y un archivo de
+configuración para las pruebas. El archivo de configuración
+especifica los criterios de cada búsqueda. Este es un ejemplo::
+
+ [uuid]
+ uuid=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
+ expected=1
+
+ [rfc_emisor]
+ rfc_emisor=XXXXXXXXXXXX
+ año=2014
+ mes=09
+ día=26
+ expected=1
+
+ [año_mes_día]
+ año=2014
+ mes=09
+ día=26
+ expected=1
+
+ [mes_completo_por_día]
+ año=2014
+ mes=09
+ expected=5
+
+Se necesitan estas cuatro secciones. Hay que ajustar los
+valores para que la cantidad de CFDIs no sea muy grande. La
+variable ``expected`` se ajusta a la cantidad de CFDIs que se
+descargan, para las credenciales que se utilicen.
+
+Para ejecutar::
+
+ python functional_DescargaSAT.py
+ ....
+ ----------------------------------------------------------------------
+ Ran 4 tests in 254.376s
+
+Agregar el parámetro ``-v`` para tener un renglón por
+cada prueba que se ejecuta::
+
+ python functional_DescargaSAT.py -v
+ test_año_mes_día (__main__.DescargaSAT) ... ok
+ test_mes_completo (__main__.DescargaSAT) ... ok
+ test_rfc (__main__.DescargaSAT) ... ok
+ test_uuid (__main__.DescargaSAT) ... ok
+
+ ----------------------------------------------------------------------
+ Ran 4 tests in 254.376s
diff --git a/functional_DescargaSAT.conf.sample b/functional_DescargaSAT.conf.sample
new file mode 100644
index 0000000..83f31cc
--- /dev/null
+++ b/functional_DescargaSAT.conf.sample
@@ -0,0 +1,21 @@
+[uuid]
+uuid=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
+expected=1
+
+[rfc_emisor]
+rfc_emisor=XXXXXXXXXXXX
+año=2014
+mes=09
+día=26
+expected=1
+
+[año_mes_día]
+año=2014
+mes=09
+día=26
+expected=1
+
+[mes_completo_por_día]
+año=2014
+mes=09
+expected=5
diff --git a/functional_DescargaSAT.py b/functional_DescargaSAT.py
new file mode 100755
index 0000000..8273567
--- /dev/null
+++ b/functional_DescargaSAT.py
@@ -0,0 +1,275 @@
+import unittest
+
+
+class DescargaSAT(unittest.TestCase):
+
+ def setUp(self):
+ import configparser
+ from admincfdi.pyutil import Util
+
+ self.config = configparser.ConfigParser()
+ self.config.read('functional_DescargaSAT.conf' )
+
+ util = Util()
+ status, self.rfc, self.ciec = util.lee_credenciales('credenciales.conf')
+ self.assertEqual('Ok', status)
+
+ def test_connect_disconnect(self):
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+
+ def no_op(*args):
+ pass
+
+ descarga = DescargaSAT(status_callback=no_op)
+ status = descarga.connect(profile, rfc=self.rfc, ciec=self.ciec)
+ self.assertTrue(status)
+ descarga.disconnect()
+
+ def test_connect_fail(self):
+ from admincfdi.pyutil import DescargaSAT
+ from selenium import webdriver
+
+ profile = webdriver.FirefoxProfile()
+
+ def no_op(*args):
+ pass
+
+ descarga = DescargaSAT(status_callback=no_op)
+ status = descarga.connect(profile, rfc='x', ciec='y')
+ self.assertFalse(status)
+ descarga.browser.close()
+
+ def test_search_uuid(self):
+ import os
+ import tempfile
+ from admincfdi.pyutil import DescargaSAT
+
+ def no_op(*args):
+ pass
+
+ seccion = self.config['uuid']
+ uuid = seccion['uuid']
+ expected = int(seccion['expected'])
+ descarga = DescargaSAT(status_callback=no_op,
+ download_callback=no_op)
+ profile = descarga.get_firefox_profile('destino')
+ descarga.connect(profile, rfc=self.rfc, ciec=self.ciec)
+ result = descarga.search(uuid=uuid,
+ día='00')
+ descarga.disconnect()
+ self.assertEqual(expected, len(result))
+
+ def test_uuid(self):
+ import os
+ import tempfile
+ from admincfdi.pyutil import DescargaSAT
+
+ def no_op(*args):
+ pass
+
+ seccion = self.config['uuid']
+ uuid = seccion['uuid']
+ expected = int(seccion['expected'])
+ descarga = DescargaSAT(status_callback=no_op,
+ download_callback=no_op)
+ with tempfile.TemporaryDirectory() as tempdir:
+ destino = os.path.join(tempdir, 'cfdi-descarga')
+ profile = descarga.get_firefox_profile(destino)
+ descarga.connect(profile, rfc=self.rfc, ciec=self.ciec)
+ docs = descarga.search(uuid=uuid, día='00')
+ descarga.download(docs)
+ descarga.disconnect()
+ self.assertEqual(expected, len(os.listdir(destino)))
+
+ def test_rfc(self):
+ import os
+ import tempfile
+ from admincfdi.pyutil import DescargaSAT
+
+ def no_op(*args):
+ pass
+
+ seccion = self.config['rfc_emisor']
+ rfc_emisor = seccion['rfc_emisor']
+ año = seccion['año']
+ mes = seccion['mes']
+ día = seccion['día']
+ expected = int(seccion['expected'])
+ descarga = DescargaSAT(status_callback=no_op,
+ download_callback=no_op)
+ with tempfile.TemporaryDirectory() as tempdir:
+ destino = os.path.join(tempdir, 'cfdi-descarga')
+ profile = descarga.get_firefox_profile(destino)
+ descarga.connect(profile, rfc=self.rfc, ciec=self.ciec)
+ docs = descarga.search(año=año, mes=mes, día=día,
+ rfc_emisor=rfc_emisor)
+ descarga.download(docs)
+ descarga.disconnect()
+ self.assertEqual(expected, len(os.listdir(destino)))
+
+ def test_año_mes_día(self):
+ import os
+ import tempfile
+ from admincfdi.pyutil import DescargaSAT
+
+ def no_op(*args):
+ pass
+
+ seccion = self.config['año_mes_día']
+ año = seccion['año']
+ mes = seccion['mes']
+ día = seccion['día']
+ expected = int(seccion['expected'])
+ descarga = DescargaSAT(status_callback=no_op,
+ download_callback=no_op)
+ with tempfile.TemporaryDirectory() as tempdir:
+ destino = os.path.join(tempdir, 'cfdi-descarga')
+ profile = descarga.get_firefox_profile(destino)
+ descarga.connect(profile, rfc=self.rfc, ciec=self.ciec)
+ docs = descarga.search(año=año, mes=mes, día=día)
+ descarga.download(docs)
+ descarga.disconnect()
+ self.assertEqual(expected, len(os.listdir(destino)))
+
+ def test_hora_minuto_inicial(self):
+ import os
+ import tempfile
+ from admincfdi.pyutil import DescargaSAT
+
+ def no_op(*args):
+ pass
+
+ seccion = self.config['hora_minuto_inicial']
+ año = seccion['año']
+ mes = seccion['mes']
+ día = seccion['día']
+ hora_inicial = seccion['hora_inicial']
+ minuto_inicial = seccion['minuto_inicial']
+ expected = int(seccion['expected'])
+ descarga = DescargaSAT(status_callback=no_op,
+ download_callback=no_op)
+ with tempfile.TemporaryDirectory() as tempdir:
+ destino = os.path.join(tempdir, 'cfdi-descarga')
+ profile = descarga.get_firefox_profile(destino)
+ descarga.connect(profile, rfc=self.rfc, ciec=self.ciec)
+ docs = descarga.search(año=año, mes=mes, día=día,
+ hora_inicial=hora_inicial,
+ minuto_inicial=minuto_inicial)
+ descarga.download(docs)
+ descarga.disconnect()
+ self.assertEqual(expected, len(os.listdir(destino)))
+
+ def test_hora_minuto_final(self):
+ import os
+ import tempfile
+ from admincfdi.pyutil import DescargaSAT
+
+ def no_op(*args):
+ pass
+
+ seccion = self.config['hora_minuto_final']
+ año = seccion['año']
+ mes = seccion['mes']
+ día = seccion['día']
+ hora_final = seccion['hora_final']
+ minuto_final = seccion['minuto_final']
+ expected = int(seccion['expected'])
+ descarga = DescargaSAT(status_callback=no_op,
+ download_callback=no_op)
+ with tempfile.TemporaryDirectory() as tempdir:
+ destino = os.path.join(tempdir, 'cfdi-descarga')
+ profile = descarga.get_firefox_profile(destino)
+ descarga.connect(profile, rfc=self.rfc, ciec=self.ciec)
+ docs = descarga.search(año=año, mes=mes, día=día,
+ hora_final=hora_final, minuto_final=minuto_final)
+ descarga.download(docs)
+ descarga.disconnect()
+ self.assertEqual(expected, len(os.listdir(destino)))
+
+ def test_segundo_inicial_final(self):
+ import os
+ import tempfile
+ from admincfdi.pyutil import DescargaSAT
+
+ def no_op(*args):
+ pass
+
+ seccion = self.config['segundo_inicial_final']
+ año = seccion['año']
+ mes = seccion['mes']
+ día = seccion['día']
+ hora_inicial = seccion['hora_inicial']
+ minuto_inicial = seccion['minuto_inicial']
+ segundo_inicial = seccion['segundo_inicial']
+ hora_final = seccion['hora_final']
+ minuto_final = seccion['minuto_final']
+ segundo_final = seccion['segundo_final']
+ expected = int(seccion['expected'])
+ descarga = DescargaSAT(status_callback=no_op,
+ download_callback=no_op)
+ with tempfile.TemporaryDirectory() as tempdir:
+ destino = os.path.join(tempdir, 'cfdi-descarga')
+ profile = descarga.get_firefox_profile(destino)
+ descarga.connect(profile, rfc=self.rfc, ciec=self.ciec)
+ docs = descarga.search(año=año, mes=mes, día=día,
+ hora_inicial=hora_inicial, minuto_inicial=minuto_inicial,
+ segundo_inicial=segundo_inicial,
+ hora_final=hora_final, minuto_final=minuto_final,
+ segundo_final=segundo_final)
+ descarga.download(docs)
+ descarga.disconnect()
+ self.assertEqual(expected, len(os.listdir(destino)))
+
+ def test_mes_completo(self):
+ import os
+ import tempfile
+ from admincfdi.pyutil import DescargaSAT
+
+ def no_op(*args):
+ pass
+
+ seccion = self.config['mes_completo_por_día']
+ año = seccion['año']
+ mes = seccion['mes']
+ expected = int(seccion['expected'])
+ descarga = DescargaSAT(status_callback=no_op,
+ download_callback=no_op)
+ with tempfile.TemporaryDirectory() as tempdir:
+ destino = os.path.join(tempdir, 'cfdi-descarga')
+ profile = descarga.get_firefox_profile(destino)
+ descarga.connect(profile, rfc=self.rfc, ciec=self.ciec)
+ docs = descarga.search(año=año, mes=mes, día='00',
+ mes_completo_por_día=True)
+ descarga.download(docs)
+ descarga.disconnect()
+ self.assertEqual(expected, len(os.listdir(destino)))
+
+ def test_emitidas(self):
+ import os
+ import tempfile
+ from admincfdi.pyutil import DescargaSAT
+
+ def no_op(*args):
+ pass
+
+ seccion = self.config['emitidas']
+ año = seccion['año']
+ mes = seccion['mes']
+ expected = int(seccion['expected'])
+ descarga = DescargaSAT(status_callback=no_op,
+ download_callback=no_op)
+ with tempfile.TemporaryDirectory() as tempdir:
+ destino = os.path.join(tempdir, 'cfdi-descarga')
+ profile = descarga.get_firefox_profile(destino)
+ descarga.connect(profile, rfc=self.rfc, ciec=self.ciec)
+ docs = descarga.search(año=año, mes=mes, día='00',
+ facturas_emitidas=True)
+ descarga.disconnect()
+ self.assertEqual(expected, len(docs))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pwd.sample b/pwd.sample
new file mode 100644
index 0000000..273bf02
--- /dev/null
+++ b/pwd.sample
@@ -0,0 +1,3 @@
+rfc ciec
+# escribe en el renglón de arriba tu RFC y tu CIEC
+# separados por un espacio
diff --git a/pyqrcode/__init__.py b/pyqrcode/__init__.py
deleted file mode 100644
index 97978b8..0000000
--- a/pyqrcode/__init__.py
+++ /dev/null
@@ -1,322 +0,0 @@
-"""This module is used to create QR Codes. It is designed to be as simple and
-as possible. It does this by using sane defaults and autodetection to make
-creating a QR Code very simple.
-
-It is recommended that you use the :func:`pyqrcode.create` function to build the
-QRCode object. This results in cleaner looking code.
-
-Examples:
- >>> import pyqrcode
- >>> import sys
- >>> url = pyqrcode.create('http://uca.edu')
- >>> url.svg(sys.stdout, scale=1)
- >>> url.svg('uca.svg', scale=4)
- >>> number = pyqrcode.create(123456789012345)
- >>> number.png('big-number.png')
-"""
-import pyqrcode.tables
-import pyqrcode.builder as builder
-
-def create(content, error='H', version=None, mode=None):
- """When creating a QR code only the content to be encoded is required,
- all the other properties of the code will be guessed based on the
- contents given. This function will return a :class:`QRCode` object.
-
- Unless you are familiar with QR code's inner workings
- it is recommended that you just specify the content and nothing else.
- However, there are cases where you may want to specify the various
- properties of the created code manually, this is what the other
- parameters do. Below, you will find a lengthy explanation of what
- each parameter is for. Note, the parameter names and values are taken
- directly from the standards. You may need to familiarize yourself
- with the terminology of QR codes for the names to make sense.
-
- The *error* parameter sets the error correction level of the code. There
- are four levels defined by the standard. The first is level 'L' which
- allows for 7% of the code to be corrected. Second, is level 'M' which
- allows for 15% of the code to be corrected. Next, is level 'Q' which
- is the most common choice for error correction, it allow 25% of the
- code to be corrected. Finally, there is the highest level 'H' which
- allows for 30% of the code to be corrected. There are several ways to
- specify this parameter, you can use an upper or lower case letter,
- a float corresponding to the percentage of correction, or a string
- containing the percentage. See tables.modes for all the possible
- values. By default this parameter is set to 'H' which is the highest
- possible error correction, but it has the smallest available data
- capacity.
-
- The *version* parameter specifies the size and data capacity of the
- code. Versions are any integer between 1 and 40. Where version 1 is
- the smallest QR code, and version 40 is the largest. If this parameter
- is left unspecified, then the contents and error correction level will
- be used to guess the smallest possible QR code version that the
- content will fit inside of. You may want to specify this parameter
- for consistency when generating several QR codes with varying amounts
- of data. That way all of the generated codes would have the same size.
-
- The *mode* parameter specifies how the contents will be encoded. By
- default, the best possible encoding for the contents is guessed. There
- are four possible encoding methods. First, is 'numeric' which is
- used to encode integer numbers. Next, is 'alphanumeric' which is
- used to encode some ASCII characters. This mode uses only a limited
- set of characters. Most problematic is that it can only use upper case
- English characters, consequently, the content parameter will be
- subjected to str.upper() before encoding. See tables.ascii_codes for
- a complete list of available characters. We then have 'binary' encoding
- which just encodes the bytes directly into the QR code (this encoding
- is the least efficient). Finally, there is 'kanji' encoding (i.e.
- Japanese characters), this encoding is unimplemented at this time.
- """
- return QRCode(content, error, version, mode)
-
-class QRCode:
- """This class represents a QR code. To use this class simply give the
- constructor a string representing the data to be encoded, it will then
- build a code in memory. You can then save it in various formats. Note,
- codes can be written out as PNG files but this requires the PyPNG module.
- You can find the PyPNG module at http://packages.python.org/pypng/.
-
- Examples:
- >>> from pyqrcode import QRCode
- >>> import sys
- >>> url = QRCode('http://uca.edu')
- >>> url.svg(sys.stdout, scale=1)
- >>> url.svg('uca.svg', scale=4)
- >>> number = QRCode(123456789012345)
- >>> number.png('big-number.png')
-
- .. note::
- For what all of the parameters do, see the :func:`pyqrcode.create`
- function.
- """
- def __init__(self, content, error='H', version=None, mode=None):
-
- #Coerce the content into a string
- self.data = str(content)
-
- #Check that the passed in error level is valid
- try:
- self.error = tables.error_level[str(error).upper()]
- except:
- raise ValueError('The error parameter is not one of '
- '"L", "M", "Q", or "H."')
-
- #Guess the mode of the code, this will also be used for
- #error checking
- guessed_content_type = self._detect_content_type()
-
- #Force a passed in mode to be lowercase
- if mode:
- mode = mode.lower()
-
- #Check that the mode parameter is compatible with the contents
- if not mode:
- #Use the guessed mode
- self.mode = guessed_content_type
- self.mode_num = tables.modes[self.mode]
- elif guessed_content_type == 'binary' and \
- tables.modes[mode] != tables.modes['binary']:
- #Binary is only guessed as a last resort, if the
- #passed in mode is not binary the data won't encode
- raise ValueError('The content provided cannot be encoded with '
- 'the mode {}, it can only be encoded as '
- 'binary.'.format(mode))
- elif tables.modes[mode] == tables.modes['numeric'] and \
- guessed_content_type != 'numeric':
- #If numeric encoding is requested make sure the data can
- #be encoded in that format
- raise ValueError('The content cannot be encoded as numeric.')
- else:
- #The data should encode with the passed in mode
- self.mode = mode
- self.mode_num = tables.modes[self.mode]
-
- #Guess the "best" version
- self.version = self._pick_best_fit()
-
- #If the user supplied a version, then check that it has
- #sufficient data capacity for the contents passed in
- if version:
- if version >= self.version:
- self.version = version
- else:
- raise ValueError('The data will not fit inside a version {} '
- 'code with the given encoding and error '
- 'level (the code must be at least a '
- 'version {}).'.format(version, self.version))
-
- #Build the QR code
- self.builder = builder.QRCodeBuilder(data=content,
- version=self.version,
- mode=self.mode,
- error=self.error)
-
- #Save the code for easier reference
- self.code = self.builder.code
-
- def __str__(self):
- return repr(self)
-
- def __repr__(self):
- return 'QRCode(content=\'{}\', error=\'{}\', version={}, mode=\'{}\')'.format(
- self.data, self.error, self.version, self.mode)
-
-
- def _detect_content_type(self):
- """This method tries to auto-detect the type of the data. It first
- tries to see if the data is a valid integer, in which case it returns
- numeric. Next, it tests the data to see if it is 'alphanumeric.' QR
- Codes use a special table with very limited range of ASCII characters.
- The code's data is tested to make sure it fits inside this limited
- range. If all else fails, the data is determined to be of type
- 'binary.'
-
- Note, encoding 'kanji' is not yet implemented.
- """
- #See if the data is an integer
- try:
- test = int(self.data)
- return 'numeric'
- except:
- #Data is not numeric, this is not an error
- pass
-
- #See if that data is alphanumeric based on the standards
- #special ASCII table
- valid_characters = tables.ascii_codes.keys()
- if all(map(lambda x: x in valid_characters, self.data.upper())):
- return 'alphanumeric'
-
- #All of the tests failed. The content can only be binary.
- return 'binary'
-
- def _pick_best_fit(self):
- """This method return the smallest possible QR code version number
- that will fit the specified data with the given error level.
- """
- for version in range(1,41):
- #Get the maximum possible capacity
- capacity = tables.data_capacity[version][self.error][self.mode_num]
-
- #Check the capacity
- if (self.mode_num == tables.modes['binary'] and \
- capacity >= len(self.data.encode('latin1'))) or \
- capacity >= len(self.data):
- return version
-
- raise ValueError('The data will not fit in any QR code version '
- 'with the given encoding and error level.')
-
- def get_png_size(self, scale):
- """This is method helps users determine what *scale* to use when
- creating a PNG of this QR code. It is meant mostly to be used in the
- console to help the user determine the pixel size of the code
- using various scales.
-
- This method will return an integer representing the width and height of
- the QR code in pixels, as if it was drawn using the given *scale*.
- Because QR codes are square, the number represents both dimensions.
-
- Example:
- >>> code = pyqrcode.QRCode("I don't like spam!")
- >>> print(code.get_png_size(1))
- 31
- >>> print(code.get_png_size(5))
- 155
- """
- return builder._get_png_size(self.version, scale)
-
- def png(self, file, scale=1, module_color=None, background=None):
- """This method writes the QR code out as an PNG image. The resulting
- PNG has a bit depth of 1. The file parameter is used to specify where
- to write the image to. It can either be an writable stream or a
- file path.
-
- .. note::
- This method depends on the pypng module to actually create the
- PNG file.
-
- This method will write the given *file* out as a PNG file. The file
- can be either a string file path, or a writable stream.
-
- The *scale* parameter sets how large to draw a single module. By
- default one pixel is used to draw a single module. This may make the
- code too small to be read efficiently. Increasing the scale will make
- the code larger. Only integer scales are usable. This method will
- attempt to coerce the parameter into an integer (e.g. 2.5 will become 2,
- and '3' will become 3).
-
- The *module_color* parameter sets what color to use for the encoded
- modules (the black part on most QR codes). The *background* parameter
- sets what color to use for the background (the white part on most
- QR codes). If either parameter is set, then both must be
- set or a ValueError is raised. Colors should be specified as either
- a list or a tuple of length 3 or 4. The components of the list must
- be integers between 0 and 255. The first three member give the RGB
- color. The fourth member gives the alpha component, where 0 is
- transparent and 255 is opaque. Note, many color
- combinations are unreadable by scanners, so be careful.
-
-
-
- Example:
- >>> code = pyqrcode.create('Are you suggesting coconuts migrate?')
- >>> code.png('swallow.png', scale=5)
- >>> code.png('swallow.png', scale=5,
- module_color=(0x66, 0x33, 0x0), #Dark brown
- background=(0xff, 0xff, 0xff, 0x88)) #50% transparent white
- """
- builder._png(self.code, self.version, file, scale,
- module_color, background)
-
- def svg(self, file, scale=1, module_color='#000000', background=None):
- """This method writes the QR code out as an SVG document. The
- code is drawn by drawing only the modules corresponding to a 1. They
- are drawn using a line, such that contiguous modules in a row
- are drawn with a single line.
-
- The *file* parameter is used to specify where to write the document
- to. It can either be a writable stream or a file path.
-
- The *scale* parameter sets how large to draw
- a single module. By default one pixel is used to draw a single
- module. This may make the code too small to be read efficiently.
- Increasing the scale will make the code larger. Unlike the png() method,
- this method will accept fractional scales (e.g. 2.5).
-
- Note, three things are done to make the code more appropriate for
- embedding in a HTML document. The "white" part of the code is actually
- transparent. The code itself has a class of "pyqrcode". The lines
- making up the QR code have a class "pyqrline". These should make the
- code easier to style using CSS.
-
- You can also set the colors directly using the *module_color* and
- *background* parameters. The *module_color* parameter sets what color to
- use for the data modules (the black part on most QR codes). The
- *background* parameter sets what color to use for the background (the
- white part on most QR codes). The parameters can be set to any valid
- SVG or HTML color. If the background is set to None, then no background
- will be drawn, i.e. the background will be transparent. Note, many color
- combinations are unreadable by scanners, so be careful.
-
- Example:
- >>> code = pyqrcode.create('Hello. Uhh, can we have your liver?')
- >>> code.svg('live-organ-transplants.svg', 3.6)
- >>> code.svg('live-organ-transplants.svg', scale=4,
- module_color='brown', background='0xFFFFFF')
- """
- builder._svg(self.code, self.version, file, scale,
- module_color, background)
-
- def text(self):
- """This method returns a string based representation of the QR code.
- The data modules are represented by 1's and the background modules are
- represented by 0's. The main purpose of this method is to allow a user
- to write their own renderer.
-
- Example:
- >>> code = pyqrcode.create('Example')
- >>> text = code.text()
- >>> print(text)
- """
- return builder._text(self.code)
diff --git a/pyqrcode/builder.py b/pyqrcode/builder.py
deleted file mode 100644
index 96ac93f..0000000
--- a/pyqrcode/builder.py
+++ /dev/null
@@ -1,1089 +0,0 @@
-"""This module does the actual generation of the QR codes. The QRCodeBuilder
-builds the code. While the various output methods draw the code into a file.
-"""
-import pyqrcode.tables as tables
-import io
-import sys
-import itertools
-
-class QRCodeBuilder:
- """This class generates a QR code based on the standard. It is meant to
- be used internally, not by users!!!
-
- This class implements the tutorials found at:
-
- * http://www.thonky.com/qr-code-tutorial/
-
- * http://www.matchadesign.com/blog/qr-code-demystified-part-6/
-
- This class also uses the standard, which can be read online at:
- http://raidenii.net/files/datasheets/misc/qr_code.pdf
-
- Test codes were tested against:
- http://zxing.org/w/decode.jspx
-
- Also, reference codes were generated at:
- http://www.morovia.com/free-online-barcode-generator/qrcode-maker.php
-
- QR code Debugger:
- http://qrlogo.kaarposoft.dk/qrdecode.html
- """
- def __init__(self, data, version, mode, error):
- """See :py:class:`pyqrcode.QRCode` for information on the parameters."""
-
- #Set what data we are going to use to generate
- #the QR code
- if isinstance(data, bytes):
- self.data = data.decode('utf-8')
- else:
- self.data = data
-
- #Check that the user passed in a valid mode
- if mode in tables.modes.keys():
- self.mode = tables.modes[mode]
- else:
- raise LookupError('{} is not a valid mode.'.format(mode))
-
- #Check that the user passed in a valid error level
- if error in tables.error_level.keys():
- self.error = tables.error_level[error]
- else:
- raise LookupError('{} is not a valid error '
- 'level.'.format(error))
-
- if 1 <= version <= 40:
- self.version = version
- else:
- raise ValueError("The version must between 1 and 40.")
-
- #Look up the proper row for error correction code words
- self.error_code_words = tables.eccwbi[version][self.error]
-
- #This property will hold the binary string as it is built
- self.buffer = io.StringIO()
-
- #Create the binary data block
- self.add_data()
-
- #Create the actual QR code
- self.make_code()
-
- def grouper(self, n, iterable, fillvalue=None):
- """This generator yields a set of tuples, where the
- iterable is broken into n sized chunks. If the
- iterable is not evenly sized then fillvalue will
- be appended to the last tuple to make up the difference.
-
- This function is copied from the standard docs on
- itertools.
- """
- args = [iter(iterable)] * n
- return itertools.zip_longest(*args, fillvalue=fillvalue)
-
- def binary_string(self, data, length):
- """This method returns a string of length n that is the binary
- representation of the given data. This function is used to
- basically create bit fields of a given size.
- """
- return '{{:0{}b}}'.format(length).format(int(data))
-
- def get_data_length(self):
- """QR codes contain a "data length" field. This method creates this
- field. A binary string representing the appropriate length is
- returned.
- """
-
- #The "data length" field varies by the type of code and its mode.
- #discover how long the "data length" field should be.
- if 1 <= self.version <= 9:
- max_version = 9
- elif 10 <= self.version <= 26:
- max_version = 26
- elif 27 <= self.version <= 40:
- max_version = 40
-
- data_length = tables.data_length_field[max_version][self.mode]
-
- length_string = self.binary_string(len(self.data), data_length)
-
- if len(length_string) > data_length:
- raise ValueError('The supplied data will not fit '
- 'within this version of a QRCode.')
- return length_string
-
- def encode(self):
- """This method encodes the data into a binary string using
- the appropriate algorithm specified by the mode.
- """
- if self.mode == tables.modes['alphanumeric']:
- encoded = self.encode_alphanumeric()
- elif self.mode == tables.modes['numeric']:
- encoded = self.encode_numeric()
- elif self.mode == tables.modes['bytes']:
- encoded = self.encode_bytes()
- else:
- raise ValueError('This mode is not yet implemented.')
-
- bits = self.terminate_bits(encoded)
- if bits is not None:
- encoded += bits
-
- return encoded
-
- def encode_alphanumeric(self):
- """This method encodes the QR code's data if its mode is
- alphanumeric. It returns the data encoded as a binary string.
- """
- #Convert the string to upper case
- self.data = self.data.upper()
-
- #Change the data such that it uses a QR code ascii table
- ascii = []
- for char in self.data:
- ascii.append(tables.ascii_codes[char])
-
- #Now perform the algorithm that will make the ascii into bit fields
- with io.StringIO() as buf:
- for (a,b) in self.grouper(2, ascii):
- if b is not None:
- buf.write(self.binary_string((45*a)+b, 11))
- else:
- #This occurs when there is an odd number
- #of characters in the data
- buf.write(self.binary_string(a, 6))
-
- #Return the binary string
- return buf.getvalue()
-
- def encode_numeric(self):
- """This method encodes the QR code's data if its mode is
- numeric. It returns the data encoded as a binary string.
- """
- with io.StringIO() as buf:
- #Break the number into groups of three digits
- for triplet in self.grouper(3, self.data):
- number = ''
- for digit in triplet:
- #Only build the string if digit is not None
- if digit:
- number = ''.join([number, digit])
- else:
- break
-
- #If the number is one digits, make a 4 bit field
- if len(number) == 1:
- bin = self.binary_string(number, 4)
-
- #If the number is two digits, make a 7 bit field
- elif len(number) == 2:
- bin = self.binary_string(number, 7)
-
- #Three digit numbers use a 10 bit field
- else:
- bin = self.binary_string(number, 10)
-
- buf.write(bin)
- return buf.getvalue()
-
- def encode_bytes(self):
- """This method encodes the QR code's data if its mode is
- 8 bit mode. It returns the data encoded as a binary string.
- """
- with io.StringIO() as buf:
- for char in self.data.encode('latin1'):
- buf.write('{{:0{}b}}'.format(8).format(char))
- return buf.getvalue()
-
-
- def add_data(self):
- """This function properly constructs a QR code's data string. It takes
- into account the interleaving pattern required by the standard.
- """
- #Encode the data into a QR code
- self.buffer.write(self.binary_string(self.mode, 4))
- self.buffer.write(self.get_data_length())
- self.buffer.write(self.encode())
-
- #delimit_words and add_words can return None
- add_bits = self.delimit_words()
- if add_bits:
- self.buffer.write(add_bits)
-
- fill_bytes = self.add_words()
- if fill_bytes:
- self.buffer.write(fill_bytes)
-
- #Get a numeric representation of the data
- data = [int(''.join(x),2)
- for x in self.grouper(8, self.buffer.getvalue())]
-
- #This is the error information for the code
- error_info = tables.eccwbi[self.version][self.error]
-
- #This will hold our data blocks
- data_blocks = []
-
- #This will hold our error blocks
- error_blocks = []
-
- #Some codes have the data sliced into two different sized blocks
- #for example, first two 14 word sized blocks, then four 15 word
- #sized blocks. This means that slicing size can change over time.
- data_block_sizes = [error_info[2]] * error_info[1]
- if error_info[3] != 0:
- data_block_sizes.extend([error_info[4]] * error_info[3])
-
- #For every block of data, slice the data into the appropriate
- #sized block
- current_byte = 0
- for n_data_blocks in data_block_sizes:
- data_blocks.append(data[current_byte:current_byte+n_data_blocks])
- current_byte += n_data_blocks
-
- if current_byte < len(data):
- raise ValueError('Too much data for this code version.')
-
- #DEBUG CODE!!!!
- #Print out the data blocks
- #print('Data Blocks:\n{}'.format(data_blocks))
-
- #Calculate the error blocks
- for n, block in enumerate(data_blocks):
- error_blocks.append(self.make_error_block(block, n))
-
- #DEBUG CODE!!!!
- #Print out the error blocks
- #print('Error Blocks:\n{}'.format(error_blocks))
-
- #Buffer we will write our data blocks into
- data_buffer = io.StringIO()
-
- #Add the data blocks
- #Write the buffer such that: block 1 byte 1, block 2 byte 1, etc.
- largest_block = max(error_info[2], error_info[4])+error_info[0]
- for i in range(largest_block):
- for block in data_blocks:
- if i < len(block):
- data_buffer.write(self.binary_string(block[i], 8))
-
- #Add the error code blocks.
- #Write the buffer such that: block 1 byte 1, block 2 byte 2, etc.
- for i in range(error_info[0]):
- for block in error_blocks:
- data_buffer.write(self.binary_string(block[i], 8))
-
- self.buffer = data_buffer
-
- def terminate_bits(self, payload):
- """This method adds zeros to the end of the encoded data so that the
- encoded data is of the correct length. It returns a binary string
- containing the bits to be added.
- """
- data_capacity = tables.data_capacity[self.version][self.error][0]
-
- if len(payload) > data_capacity:
- raise ValueError('The supplied data will not fit '
- 'within this version of a QR code.')
-
- #We must add up to 4 zeros to make up for any shortfall in the
- #length of the data field.
- if len(payload) == data_capacity:
- return None
- elif len(payload) <= data_capacity-4:
- bits = self.binary_string(0,4)
- else:
- #Make up any shortfall need with less than 4 zeros
- bits = self.binary_string(0, data_capacity - len(payload))
-
- return bits
-
- def delimit_words(self):
- """This method takes the existing encoded binary string
- and returns a binary string that will pad it such that
- the encoded string contains only full bytes.
- """
- bits_short = 8 - (len(self.buffer.getvalue()) % 8)
-
- #The string already falls on an byte boundary do nothing
- if bits_short == 8:
- return None
- else:
- return self.binary_string(0, bits_short)
-
- def add_words(self):
- """The data block must fill the entire data capacity of the QR code.
- If we fall short, then we must add bytes to the end of the encoded
- data field. The value of these bytes are specified in the standard.
- """
-
- data_blocks = len(self.buffer.getvalue()) // 8
- total_blocks = tables.data_capacity[self.version][self.error][0] // 8
- needed_blocks = total_blocks - data_blocks
-
- if needed_blocks == 0:
- return None
-
- #This will return item1, item2, item1, item2, etc.
- block = itertools.cycle(['11101100', '00010001'])
-
- #Create a string of the needed blocks
- return ''.join([next(block) for x in range(needed_blocks)])
-
- def _fix_exp(self, exponent):
- """Makes sure the exponent ranges from 0 to 255."""
- #return (exponent % 256) + (exponent // 256)
- return exponent % 255
-
- def make_error_block(self, block, block_number):
- """This function constructs the error correction block of the
- given data block. This is *very complicated* process. To
- understand the code you need to read:
-
- * http://www.thonky.com/qr-code-tutorial/part-2-error-correction/
- * http://www.matchadesign.com/blog/qr-code-demystified-part-4/
- """
- #Get the error information from the standards table
- error_info = tables.eccwbi[self.version][self.error]
-
- #This is the number of 8-bit words per block
- if block_number < error_info[1]:
- code_words_per_block = error_info[2]
- else:
- code_words_per_block = error_info[4]
-
- #This is the size of the error block
- error_block_size = error_info[0]
-
- #Copy the block as the message polynomial coefficients
- mp_co = block[:]
-
- #Add the error blocks to the message polynomial
- mp_co.extend([0] * (error_block_size))
-
- #Get the generator polynomial
- generator = tables.generator_polynomials[error_block_size]
-
- #This will hold the temporary sum of the message coefficient and the
- #generator polynomial
- gen_result = [0] * len(generator)
-
- #Go through every code word in the block
- for i in range(code_words_per_block):
- #Get the first coefficient from the message polynomial
- coefficient = mp_co.pop(0)
-
- #Skip coefficients that are zero
- if coefficient == 0:
- continue
- else:
- #Turn the coefficient into an alpha exponent
- alpha_exp = tables.galois_antilog[coefficient]
-
- #Add the alpha to the generator polynomial
- for n in range(len(generator)):
- gen_result[n] = alpha_exp + generator[n]
- if gen_result[n] > 255:
- gen_result[n] = gen_result[n] % 255
-
- #Convert the alpha notation back into coefficients
- gen_result[n] = tables.galois_log[gen_result[n]]
-
- #XOR the sum with the message coefficients
- mp_co[n] = gen_result[n] ^ mp_co[n]
-
- #Pad the end of the error blocks with zeros if needed
- if len(mp_co) < code_words_per_block:
- mp_co.extend([0] * (code_words_per_block - len(mp_co)))
-
- return mp_co
-
- def make_code(self):
- """This method returns the best possible QR code."""
- from copy import deepcopy
-
- #Get the size of the underlying matrix
- matrix_size = tables.version_size[self.version]
-
- #Create a template matrix we will build the codes with
- row = [' ' for x in range(matrix_size)]
- template = [deepcopy(row) for x in range(matrix_size)]
-
- #Add mandatory information to the template
- self.add_detection_pattern(template)
- self.add_position_pattern(template)
- self.add_version_pattern(template)
-
- #Create the various types of masks of the template
- self.masks = self.make_masks(template)
-
- self.best_mask = self.choose_best_mask()
- self.code = self.masks[self.best_mask]
-
- def add_detection_pattern(self, m):
- """This method add the detection patterns to the QR code. This lets
- the scanner orient the pattern. It is required for all QR codes.
- The detection pattern consists of three boxes located at the upper
- left, upper right, and lower left corners of the matrix. Also, two
- special lines called the timing pattern is also necessary. Finally,
- a single black pixel is added just above the lower left black box.
- """
-
- #Draw outer black box
- for i in range(7):
- inv = -(i+1)
- for j in [0,6,-1,-7]:
- m[j][i] = 1
- m[i][j] = 1
- m[inv][j] = 1
- m[j][inv] = 1
-
- #Draw inner white box
- for i in range(1, 6):
- inv = -(i+1)
- for j in [1, 5, -2, -6]:
- m[j][i] = 0
- m[i][j] = 0
- m[inv][j] = 0
- m[j][inv] = 0
-
- #Draw inner black box
- for i in range(2, 5):
- for j in range(2, 5):
- inv = -(i+1)
- m[i][j] = 1
- m[inv][j] = 1
- m[j][inv] = 1
-
- #Draw white border
- for i in range(8):
- inv = -(i+1)
- for j in [7, -8]:
- m[i][j] = 0
- m[j][i] = 0
- m[inv][j] = 0
- m[j][inv] = 0
-
- #To keep the code short, it draws an extra box
- #in the lower right corner, this removes it.
- for i in range(-8, 0):
- for j in range(-8, 0):
- m[i][j] = ' '
-
- #Add the timing pattern
- bit = itertools.cycle([1,0])
- for i in range(8, (len(m)-8)):
- b = next(bit)
- m[i][6] = b
- m[6][i] = b
-
- #Add the extra black pixel
- m[-8][8] = 1
-
- def add_position_pattern(self, m):
- """This method draws the position adjustment patterns onto the QR
- Code. All QR code versions larger than one require these special boxes
- called position adjustment patterns.
- """
- #Version 1 does not have a position adjustment pattern
- if self.version == 1:
- return
-
- #Get the coordinates for where to place the boxes
- coordinates = tables.position_adjustment[self.version]
-
- #Get the max and min coordinates to handle special cases
- min_coord = coordinates[0]
- max_coord = coordinates[-1]
-
- #Draw a box at each intersection of the coordinates
- for i in coordinates:
- for j in coordinates:
- #Do not draw these boxes because they would
- #interfere with the detection pattern
- if (i == min_coord and j == min_coord) or \
- (i == min_coord and j == max_coord) or \
- (i == max_coord and j == min_coord):
- continue
-
- #Center black pixel
- m[i][j] = 1
-
- #Surround the pixel with a white box
- for x in [-1,1]:
- m[i+x][j+x] = 0
- m[i+x][j] = 0
- m[i][j+x] = 0
- m[i-x][j+x] = 0
- m[i+x][j-x] = 0
-
- #Surround the white box with a black box
- for x in [-2,2]:
- for y in [0,-1,1]:
- m[i+x][j+x] = 1
- m[i+x][j+y] = 1
- m[i+y][j+x] = 1
- m[i-x][j+x] = 1
- m[i+x][j-x] = 1
-
- def add_version_pattern(self, m):
- """For QR codes with a version 7 or higher, a special pattern
- specifying the code's version is required.
-
- For further information see:
- http://www.thonky.com/qr-code-tutorial/format-version-information/#example-of-version-7-information-string
- """
- if self.version < 7:
- return
-
- #Get the bit fields for this code's version
- #We will iterate across the string, the bit string
- #needs the least significant digit in the zero-th position
- field = iter(tables.version_pattern[self.version][::-1])
-
- #Where to start placing the pattern
- start = len(m)-11
-
- #The version pattern is pretty odd looking
- for i in range(6):
- #The pattern is three modules wide
- for j in range(start, start+3):
- bit = int(next(field))
-
- #Bottom Left
- m[i][j] = bit
-
- #Upper right
- m[j][i] = bit
-
- def make_masks(self, template):
- """This method generates all seven masks so that the best mask can
- be determined. The template parameter is a code matrix that will
- server as the base for all the generated masks.
- """
- from copy import deepcopy
-
- nmasks = len(tables.mask_patterns)
- masks = [''] * nmasks
- count = 0
-
- for n in range(nmasks):
- cur_mask = deepcopy(template)
- masks[n] = cur_mask
-
- #Add the type pattern bits to the code
- self.add_type_pattern(cur_mask, tables.type_bits[self.error][n])
-
- #Get the mask pattern
- pattern = tables.mask_patterns[n]
-
- #This will read the 1's and 0's one at a time
- bits = iter(self.buffer.getvalue())
-
- #These will help us do the up, down, up, down pattern
- row_start = itertools.cycle([len(cur_mask)-1, 0])
- row_stop = itertools.cycle([-1,len(cur_mask)])
- direction = itertools.cycle([-1, 1])
-
- #The data pattern is added using pairs of columns
- for column in range(len(cur_mask)-1, 0, -2):
-
- #The vertical timing pattern is an exception to the rules,
- #move the column counter over by one
- if column <= 6:
- column = column - 1
-
- #This will let us fill in the pattern
- #right-left, right-left, etc.
- column_pair = itertools.cycle([column, column-1])
-
- #Go through each row in the pattern moving up, then down
- for row in range(next(row_start), next(row_stop),
- next(direction)):
-
- #Fill in the right then left column
- for i in range(2):
- col = next(column_pair)
-
- #Go to the next column if we encounter a
- #preexisting pattern (usually an alignment pattern)
- if cur_mask[row][col] != ' ':
- continue
-
- #Some versions don't have enough bits. You then fill
- #in the rest of the pattern with 0's. These are
- #called "remainder bits."
- try:
- bit = int(next(bits))
- except:
- bit = 0
-
-
- #If the pattern is True then flip the bit
- if pattern(row, col):
- cur_mask[row][col] = bit ^ 1
- else:
- cur_mask[row][col] = bit
-
- #DEBUG CODE!!!
- #Save all of the masks as png files
- #for i, m in enumerate(masks):
- # _png(m, self.version, 'mask-{}.png'.format(i), 5)
-
- return masks
-
- def choose_best_mask(self):
- """This method returns the index of the "best" mask as defined by
- having the lowest total penalty score. The penalty rules are defined
- by the standard. The mask with the lowest total score should be the
- easiest to read by optical scanners.
- """
- self.scores = []
- for n in range(len(self.masks)):
- self.scores.append([0,0,0,0])
-
- #Score penalty rule number 1
- #Look for five consecutive squares with the same color.
- #Each one found gets a penalty of 3 + 1 for every
- #same color square after the first five in the row.
- for (n, mask) in enumerate(self.masks):
- current = mask[0][0]
- counter = 0
- total = 0
-
- #Examine the mask row wise
- for row in range(0,len(mask)):
- counter = 0
- for col in range(0,len(mask)):
- bit = mask[row][col]
-
- if bit == current:
- counter += 1
- else:
- if counter >= 5:
- total += (counter - 5) + 3
- counter = 1
- current = bit
- if counter >= 5:
- total += (counter - 5) + 3
-
- #Examine the mask column wise
- for col in range(0,len(mask)):
- counter = 0
- for row in range(0,len(mask)):
- bit = mask[row][col]
-
- if bit == current:
- counter += 1
- else:
- if counter >= 5:
- total += (counter - 5) + 3
- counter = 1
- current = bit
- if counter >= 5:
- total += (counter - 5) + 3
-
- self.scores[n][0] = total
-
- #Score penalty rule 2
- #This rule will add 3 to the score for each 2x2 block of the same
- #colored pixels there are.
- for (n, mask) in enumerate(self.masks):
- count = 0
- #Don't examine the 0th and Nth row/column
- for i in range(0, len(mask)-1):
- for j in range(0, len(mask)-1):
- if mask[i][j] == mask[i+1][j] and \
- mask[i][j] == mask[i][j+1] and \
- mask[i][j] == mask[i+1][j+1]:
- count += 1
-
- self.scores[n][1] = count * 3
-
- #Score penalty rule 3
- #This rule looks for 1011101 within the mask prefixed
- #and/or suffixed by four zeros.
- patterns = [[0,0,0,0,1,0,1,1,1,0,1],
- [1,0,1,1,1,0,1,0,0,0,0],]
- #[0,0,0,0,1,0,1,1,1,0,1,0,0,0,0]]
-
- for (n, mask) in enumerate(self.masks):
- nmatches = 0
-
- for i in range(len(mask)):
- for j in range(len(mask)):
- for pattern in patterns:
- match = True
- k = j
- #Look for row matches
- for p in pattern:
- if k >= len(mask) or mask[i][k] != p:
- match = False
- break
- k += 1
- if match:
- nmatches += 1
-
- match = True
- k = j
- #Look for column matches
- for p in pattern:
- if k >= len(mask) or mask[k][i] != p:
- match = False
- break
- k += 1
- if match:
- nmatches += 1
-
-
- self.scores[n][2] = nmatches * 40
-
- #Score the last rule, penalty rule 4. This rule measures how close
- #the pattern is to being 50% black. The further it deviates from
- #this this ideal the higher the penalty.
- for (n, mask) in enumerate(self.masks):
- nblack = 0
- for row in mask:
- nblack += sum(row)
-
- total_pixels = len(mask)**2
- ratio = nblack / total_pixels
- percent = (ratio * 100) - 50
- self.scores[n][3] = int((abs(int(percent)) / 5) * 10)
-
-
- #Calculate the total for each score
- totals = [0] * len(self.scores)
- for i in range(len(self.scores)):
- for j in range(len(self.scores[i])):
- totals[i] += self.scores[i][j]
-
- #DEBUG CODE!!!
- #Prints out a table of scores
- #print('Rule Scores\n 1 2 3 4 Total')
- #for i in range(len(self.scores)):
- # print(i, end='')
- # for s in self.scores[i]:
- # print('{: >6}'.format(s), end='')
- # print('{: >7}'.format(totals[i]))
- #print('Mask Chosen: {}'.format(totals.index(min(totals))))
-
- #The lowest total wins
- return totals.index(min(totals))
-
- def add_type_pattern(self, m, type_bits):
- """This will add the pattern to the QR code that represents the error
- level and the type of mask used to make the code.
- """
- field = iter(type_bits)
- for i in range(7):
- bit = int(next(field))
-
- #Skip the timing bits
- if i < 6:
- m[8][i] = bit
- else:
- m[8][i+1] = bit
-
- if -8 < -(i+1):
- m[-(i+1)][8] = bit
-
- for i in range(-8,0):
- bit = int(next(field))
-
- m[8][i] = bit
-
- i = -i
- #Skip timing column
- if i > 6:
- m[i][8] = bit
- else:
- m[i-1][8] = bit
-
-##############################################################################
-##############################################################################
-#
-# Output Functions
-#
-##############################################################################
-##############################################################################
-
-def _get_file(file, mode):
- """This method returns the file parameter if it is an open writable
- stream. Otherwise it treats the file parameter as a file path and
- opens it with the given mode. It is used by the svg and png methods
- to interpret the file parameter.
- """
- import os.path
- #See if the file parameter is a stream
- if not isinstance(file, io.IOBase):
- #If it is not a stream open a the file path
- return open(os.path.abspath(file), mode)
- elif not file.writable():
- raise ValueError('Stream is not writable.')
- else:
- return file
-
-def _get_png_size(version, scale):
- """See: QRCode.get_png_size
-
- This function was abstracted away from QRCode to allow for the output of
- QR codes during the build process, i.e. for debugging. It works
- just the same except you must specify the code's version. This is needed
- to calculate the PNG's size.
- """
- #Formula: scale times number of modules plus the border on each side
- return (scale * tables.version_size[version]) + (2 * scale)
-
-def _text(code):
- """This method returns a text based representation of the QR code.
- This is useful for debugging purposes.
- """
- buf = io.StringIO()
-
- border_row = '0' * (len(code[0]) + 2)
-
- buf.write(border_row)
- buf.write('\n')
- for row in code:
- buf.write('0')
- for bit in row:
- if bit == 1:
- buf.write('1')
- elif bit == 0:
- buf.write('0')
- #This is for debugging unfinished QR codes,
- #unset pixels will be spaces.
- else:
- buf.write(' ')
- buf.write('0\n')
-
- buf.write(border_row)
-
- return buf.getvalue()
-
-def _svg(code, version, file, scale=1, module_color='black', background=None):
- """This method writes the QR code out as an SVG document. The
- code is drawn by drawing only the modules corresponding to a 1. They
- are drawn using a line, such that contiguous modules in a row
- are drawn with a single line. The file parameter is used to
- specify where to write the document to. It can either be an writable
- stream or a file path. The scale parameter is sets how large to draw
- a single module. By default one pixel is used to draw a single
- module. This may make the code to small to be read efficiently.
- Increasing the scale will make the code larger. This method will accept
- fractional scales (e.g. 2.5).
- """
- #This is the template for the svg line. It is placed here so it
- #does not need to be recreated for each call to line().
- line_template = '''
- '''
-
- def line(x1, y1, x2, y2, color):
- """This sub-function draws the modules. It attempts to draw them
- as a single line per row, rather than as individual rectangles.
- It uses the l variable as a template t
- """
- return line_template.format(x1+scale, y1+scale, x2+scale, y2+scale,
- color, scale)
-
- file = _get_file(file, 'w')
-
- #Write the document header
- file.write("""
-
-
- \n")
-
-def _png(code, version, file, scale=1, module_color=None, background=None):
- """See: pyqrcode.QRCode.png()
-
- This function was abstracted away from QRCode to allow for the output of
- QR codes during the build process, i.e. for debugging. It works
- just the same except you must specify the code's version. This is needed
- to calculate the PNG's size.
-
- This method will write the given file out as a PNG file. Note, it
- depends on the PyPNG module to do this.
- """
- from . import png
-
- #Coerce scale parameter into an integer
- try:
- scale = int(scale)
- except ValueError:
- raise ValueError('The scale parameter must be an integer')
-
- def scale_code(code):
- """To perform the scaling we need to inflate the number of bits.
- The PNG library expects all of the bits when it draws the PNG.
- Effectively, we double, tripple, etc. the number of columns and
- the number of rows.
- """
- #This is the row to show up at the top and bottom border
- border_module = [1] * scale
- border_row = [1] * _get_png_size(version, scale)
- border = [border_row] * scale
-
-
- #This is one row's worth of each possible module
- #PNG's use 0 for black and 1 for white, this is the
- #reverse of the QR standard
- black = [0] * scale
- white = [1] * scale
-
- #This will hold the final PNG's bits
- bits = []
-
- #Add scale rows before the code as a border,
- #as per the standard
- bits.extend(border)
-
- #Add each row of the to the final PNG bits
- for row in code:
- tmp_row = []
-
- #Add one all white module to the beginning
- #to create the vertical border
- tmp_row.extend(border_module)
-
- #Go through each bit in the code
- for item in row:
- #Add one scaled module
- if item == 0:
- tmp_row.extend(white)
- else:
- tmp_row.extend(black)
-
- #Add one all white module to the end
- #to create the vertical border
- tmp_row.extend(border_module)
-
- #Copy each row scale times
- for n in range(scale):
- bits.append(tmp_row)
-
- #Add the bottom border
- bits.extend(border)
-
- return bits
-
- def png_pallete_color(color):
- """This creates a palette color from a list or tuple. The list or
- tuple must be of length 3 (for rgb) or 4 (for rgba). The values
- must be between 0 and 255. Note rgb colors will be given an added
- alpha component set to 255.
-
- The pallete color is represented as a list, this is what is returned.
- """
- if color:
- rgba = []
- if not (3 <= len(color) <= 4):
- raise ValueError('Colors must be a list or tuple of length '
- ' 3 or 4. You passed in '
- '"{}".'.format(color))
-
- for c in color:
- c = int(c)
- if 0 <= c <= 255:
- rgba.append(int(c))
- else:
- raise ValueError('Color components must be between '
- ' 0 and 255')
-
- #Make all all colors have an alpha channel
- if len(rgba) == 3:
- rgba.append(255)
-
- return rgba
-
- #If the user passes in one parameter, then they must pass in both or neither
- #Note, this is a logical xor
- if (not module_color) != (not background):
- raise ValueError('If you specify either the black or white parameter, '
- 'then you must specify both.')
-
- #Create the pallete, or set greyscale to True
- if module_color:
- palette = [png_pallete_color(module_color),
- png_pallete_color(background)]
- greyscale = False
- else:
- palette = None
- greyscale = True
-
- #The size of the PNG
- size = _get_png_size(version, scale)
-
- #We need to increase the size of the code to match up to the
- #scale parameter.
- code = scale_code(code)
-
- #Write out the PNG
- with _get_file(file, 'wb') as f:
- w = png.Writer(width=size, height=size, greyscale=greyscale,
- palette=palette, bitdepth=1)
-
- w.write(f, code)
diff --git a/pyqrcode/png.py b/pyqrcode/png.py
deleted file mode 100644
index fc1f469..0000000
--- a/pyqrcode/png.py
+++ /dev/null
@@ -1,3838 +0,0 @@
-#!/usr/bin/env python
-
-# png.py - PNG encoder/decoder in pure Python
-#
-# Copyright (C) 2006 Johann C. Rocholl
-# Portions Copyright (C) 2009 David Jones
-# And probably portions Copyright (C) 2006 Nicko van Someren
-#
-# Original concept by Johann C. Rocholl.
-#
-# LICENCE (MIT)
-#
-# Permission is hereby granted, free of charge, to any person
-# obtaining a copy of this software and associated documentation files
-# (the "Software"), to deal in the Software without restriction,
-# including without limitation the rights to use, copy, modify, merge,
-# publish, distribute, sublicense, and/or sell copies of the Software,
-# and to permit persons to whom the Software is furnished to do so,
-# subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
-# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
-# Changelog (recent first):
-# 2009-03-11 David: interlaced bit depth < 8 (writing).
-# 2009-03-10 David: interlaced bit depth < 8 (reading).
-# 2009-03-04 David: Flat and Boxed pixel formats.
-# 2009-02-26 David: Palette support (writing).
-# 2009-02-23 David: Bit-depths < 8; better PNM support.
-# 2006-06-17 Nicko: Reworked into a class, faster interlacing.
-# 2006-06-17 Johann: Very simple prototype PNG decoder.
-# 2006-06-17 Nicko: Test suite with various image generators.
-# 2006-06-17 Nicko: Alpha-channel, grey-scale, 16-bit/plane support.
-# 2006-06-15 Johann: Scanline iterator interface for large input files.
-# 2006-06-09 Johann: Very simple prototype PNG encoder.
-
-# Incorporated into Bangai-O Development Tools by drj on 2009-02-11 from
-# http://trac.browsershots.org/browser/trunk/pypng/lib/png.py?rev=2885
-
-# Incorporated into pypng by drj on 2009-03-12 from
-# //depot/prj/bangaio/master/code/png.py#67
-
-
-"""
-Pure Python PNG Reader/Writer
-
-This Python module implements support for PNG images (see PNG
-specification at http://www.w3.org/TR/2003/REC-PNG-20031110/ ). It reads
-and writes PNG files with all allowable bit depths (1/2/4/8/16/24/32/48/64
-bits per pixel) and colour combinations: greyscale (1/2/4/8/16 bit); RGB,
-RGBA, LA (greyscale with alpha) with 8/16 bits per channel; colour mapped
-images (1/2/4/8 bit). Adam7 interlacing is supported for reading and
-writing. A number of optional chunks can be specified (when writing)
-and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``.
-
-For help, type ``import png; help(png)`` in your python interpreter.
-
-A good place to start is the :class:`Reader` and :class:`Writer` classes.
-
-Requires Python 2.3. Limited support is available for Python 2.2, but
-not everything works. Best with Python 2.4 and higher. Installation is
-trivial, but see the ``README.txt`` file (with the source distribution)
-for details.
-
-This file can also be used as a command-line utility to convert
-`Netpbm `_ PNM files to PNG, and the reverse conversion from PNG to
-PNM. The interface is similar to that of the ``pnmtopng`` program from
-Netpbm. Type ``python png.py --help`` at the shell prompt
-for usage and a list of options.
-
-A note on spelling and terminology
-----------------------------------
-
-Generally British English spelling is used in the documentation. So
-that's "greyscale" and "colour". This not only matches the author's
-native language, it's also used by the PNG specification.
-
-The major colour models supported by PNG (and hence by PyPNG) are:
-greyscale, RGB, greyscale--alpha, RGB--alpha. These are sometimes
-referred to using the abbreviations: L, RGB, LA, RGBA. In this case
-each letter abbreviates a single channel: *L* is for Luminance or Luma or
-Lightness which is the channel used in greyscale images; *R*, *G*, *B* stand
-for Red, Green, Blue, the components of a colour image; *A* stands for
-Alpha, the opacity channel (used for transparency effects, but higher
-values are more opaque, so it makes sense to call it opacity).
-
-A note on formats
------------------
-
-When getting pixel data out of this module (reading) and presenting
-data to this module (writing) there are a number of ways the data could
-be represented as a Python value. Generally this module uses one of
-three formats called "flat row flat pixel", "boxed row flat pixel", and
-"boxed row boxed pixel". Basically the concern is whether each pixel
-and each row comes in its own little tuple (box), or not.
-
-Consider an image that is 3 pixels wide by 2 pixels high, and each pixel
-has RGB components:
-
-Boxed row flat pixel::
-
- list([R,G,B, R,G,B, R,G,B],
- [R,G,B, R,G,B, R,G,B])
-
-Each row appears as its own list, but the pixels are flattened so that
-three values for one pixel simply follow the three values for the previous
-pixel. This is the most common format used, because it provides a good
-compromise between space and convenience. PyPNG regards itself as
-at liberty to replace any sequence type with any sufficiently compatible
-other sequence type; in practice each row is an array (from the array
-module), and the outer list is sometimes an iterator rather than an
-explicit list (so that streaming is possible).
-
-Flat row flat pixel::
-
- [R,G,B, R,G,B, R,G,B,
- R,G,B, R,G,B, R,G,B]
-
-The entire image is one single giant sequence of colour values.
-Generally an array will be used (to save space), not a list.
-
-Boxed row boxed pixel::
-
- list([ (R,G,B), (R,G,B), (R,G,B) ],
- [ (R,G,B), (R,G,B), (R,G,B) ])
-
-Each row appears in its own list, but each pixel also appears in its own
-tuple. A serious memory burn in Python.
-
-In all cases the top row comes first, and for each row the pixels are
-ordered from left-to-right. Within a pixel the values appear in the
-order, R-G-B-A (or L-A for greyscale--alpha).
-
-There is a fourth format, mentioned because it is used internally,
-is close to what lies inside a PNG file itself, and has some support
-from the public API. This format is called packed. When packed,
-each row is a sequence of bytes (integers from 0 to 255), just as
-it is before PNG scanline filtering is applied. When the bit depth
-is 8 this is essentially the same as boxed row flat pixel; when the
-bit depth is less than 8, several pixels are packed into each byte;
-when the bit depth is 16 (the only value more than 8 that is supported
-by the PNG image format) each pixel value is decomposed into 2 bytes
-(and `packed` is a misnomer). This format is used by the
-:meth:`Writer.write_packed` method. It isn't usually a convenient
-format, but may be just right if the source data for the PNG image
-comes from something that uses a similar format (for example, 1-bit
-BMPs, or another PNG file).
-
-And now, my famous members
---------------------------
-"""
-
-# http://www.python.org/doc/2.2.3/whatsnew/node5.html
-
-
-__version__ = "0.0.16"
-
-from array import array
-from functools import reduce
-try: # See :pyver:old
- import itertools
-except:
- pass
-import math
-# http://www.python.org/doc/2.4.4/lib/module-operator.html
-import operator
-import struct
-import sys
-import zlib
-# http://www.python.org/doc/2.4.4/lib/module-warnings.html
-import warnings
-try:
- import pyximport
- pyximport.install()
- import cpngfilters as pngfilters
-except ImportError:
- pass
-
-
-__all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array']
-
-
-# The PNG signature.
-# http://www.w3.org/TR/PNG/#5PNG-file-signature
-_signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10)
-
-_adam7 = ((0, 0, 8, 8),
- (4, 0, 8, 8),
- (0, 4, 4, 8),
- (2, 0, 4, 4),
- (0, 2, 2, 4),
- (1, 0, 2, 2),
- (0, 1, 1, 2))
-
-def group(s, n):
- # See
- # http://www.python.org/doc/2.6/library/functions.html#zip
- return list(zip(*[iter(s)]*n))
-
-def isarray(x):
- """Same as ``isinstance(x, array)`` except on Python 2.2, where it
- always returns ``False``. This helps PyPNG work on Python 2.2.
- """
-
- try:
- return isinstance(x, array)
- except:
- return False
-
-try: # see :pyver:old
- array.tostring
-except:
- def tostring(row):
- l = len(row)
- return struct.pack('%dB' % l, *row)
-else:
- def tostring(row):
- """Convert row of bytes to string. Expects `row` to be an
- ``array``.
- """
- return row.tostring()
-
-# Conditionally convert to bytes. Works on Python 2 and Python 3.
-try:
- bytes('', 'ascii')
- def strtobytes(x): return bytes(x, 'iso8859-1')
- def bytestostr(x): return str(x, 'iso8859-1')
-except:
- strtobytes = str
- bytestostr = str
-
-def interleave_planes(ipixels, apixels, ipsize, apsize):
- """
- Interleave (colour) planes, e.g. RGB + A = RGBA.
-
- Return an array of pixels consisting of the `ipsize` elements of data
- from each pixel in `ipixels` followed by the `apsize` elements of data
- from each pixel in `apixels`. Conventionally `ipixels` and
- `apixels` are byte arrays so the sizes are bytes, but it actually
- works with any arrays of the same type. The returned array is the
- same type as the input arrays which should be the same type as each other.
- """
-
- itotal = len(ipixels)
- atotal = len(apixels)
- newtotal = itotal + atotal
- newpsize = ipsize + apsize
- # Set up the output buffer
- # See http://www.python.org/doc/2.4.4/lib/module-array.html#l2h-1356
- out = array(ipixels.typecode)
- # It's annoying that there is no cheap way to set the array size :-(
- out.extend(ipixels)
- out.extend(apixels)
- # Interleave in the pixel data
- for i in range(ipsize):
- out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize]
- for i in range(apsize):
- out[i+ipsize:newtotal:newpsize] = apixels[i:atotal:apsize]
- return out
-
-def check_palette(palette):
- """Check a palette argument (to the :class:`Writer` class) for validity.
- Returns the palette as a list if okay; raises an exception otherwise.
- """
-
- # None is the default and is allowed.
- if palette is None:
- return None
-
- p = list(palette)
- if not (0 < len(p) <= 256):
- raise ValueError("a palette must have between 1 and 256 entries")
- seen_triple = False
- for i,t in enumerate(p):
- if len(t) not in (3,4):
- raise ValueError(
- "palette entry %d: entries must be 3- or 4-tuples." % i)
- if len(t) == 3:
- seen_triple = True
- if seen_triple and len(t) == 4:
- raise ValueError(
- "palette entry %d: all 4-tuples must precede all 3-tuples" % i)
- for x in t:
- if int(x) != x or not(0 <= x <= 255):
- raise ValueError(
- "palette entry %d: values must be integer: 0 <= x <= 255" % i)
- return p
-
-class Error(Exception):
- prefix = 'Error'
- def __str__(self):
- return self.prefix + ': ' + ' '.join(self.args)
-
-class FormatError(Error):
- """Problem with input file format. In other words, PNG file does
- not conform to the specification in some way and is invalid.
- """
-
- prefix = 'FormatError'
-
-class ChunkError(FormatError):
- prefix = 'ChunkError'
-
-
-class Writer:
- """
- PNG encoder in pure Python.
- """
-
- def __init__(self, width=None, height=None,
- size=None,
- greyscale=False,
- alpha=False,
- bitdepth=8,
- palette=None,
- transparent=None,
- background=None,
- gamma=None,
- compression=None,
- interlace=False,
- bytes_per_sample=None, # deprecated
- planes=None,
- colormap=None,
- maxval=None,
- chunk_limit=2**20):
- """
- Create a PNG encoder object.
-
- Arguments:
-
- width, height
- Image size in pixels, as two separate arguments.
- size
- Image size (w,h) in pixels, as single argument.
- greyscale
- Input data is greyscale, not RGB.
- alpha
- Input data has alpha channel (RGBA or LA).
- bitdepth
- Bit depth: from 1 to 16.
- palette
- Create a palette for a colour mapped image (colour type 3).
- transparent
- Specify a transparent colour (create a ``tRNS`` chunk).
- background
- Specify a default background colour (create a ``bKGD`` chunk).
- gamma
- Specify a gamma value (create a ``gAMA`` chunk).
- compression
- zlib compression level: 0 (none) to 9 (more compressed); default: -1 or None.
- interlace
- Create an interlaced image.
- chunk_limit
- Write multiple ``IDAT`` chunks to save memory.
-
- The image size (in pixels) can be specified either by using the
- `width` and `height` arguments, or with the single `size`
- argument. If `size` is used it should be a pair (*width*,
- *height*).
-
- `greyscale` and `alpha` are booleans that specify whether
- an image is greyscale (or colour), and whether it has an
- alpha channel (or not).
-
- `bitdepth` specifies the bit depth of the source pixel values.
- Each source pixel value must be an integer between 0 and
- ``2**bitdepth-1``. For example, 8-bit images have values
- between 0 and 255. PNG only stores images with bit depths of
- 1,2,4,8, or 16. When `bitdepth` is not one of these values,
- the next highest valid bit depth is selected, and an ``sBIT``
- (significant bits) chunk is generated that specifies the original
- precision of the source image. In this case the supplied pixel
- values will be rescaled to fit the range of the selected bit depth.
-
- The details of which bit depth / colour model combinations the
- PNG file format supports directly, are somewhat arcane
- (refer to the PNG specification for full details). Briefly:
- "small" bit depths (1,2,4) are only allowed with greyscale and
- colour mapped images; colour mapped images cannot have bit depth
- 16.
-
- For colour mapped images (in other words, when the `palette`
- argument is specified) the `bitdepth` argument must match one of
- the valid PNG bit depths: 1, 2, 4, or 8. (It is valid to have a
- PNG image with a palette and an ``sBIT`` chunk, but the meaning
- is slightly different; it would be awkward to press the
- `bitdepth` argument into service for this.)
-
- The `palette` option, when specified, causes a colour mapped image
- to be created: the PNG colour type is set to 3; greyscale
- must not be set; alpha must not be set; transparent must
- not be set; the bit depth must be 1,2,4, or 8. When a colour
- mapped image is created, the pixel values are palette indexes
- and the `bitdepth` argument specifies the size of these indexes
- (not the size of the colour values in the palette).
-
- The palette argument value should be a sequence of 3- or
- 4-tuples. 3-tuples specify RGB palette entries; 4-tuples
- specify RGBA palette entries. If both 4-tuples and 3-tuples
- appear in the sequence then all the 4-tuples must come
- before all the 3-tuples. A ``PLTE`` chunk is created; if there
- are 4-tuples then a ``tRNS`` chunk is created as well. The
- ``PLTE`` chunk will contain all the RGB triples in the same
- sequence; the ``tRNS`` chunk will contain the alpha channel for
- all the 4-tuples, in the same sequence. Palette entries
- are always 8-bit.
-
- If specified, the `transparent` and `background` parameters must
- be a tuple with three integer values for red, green, blue, or
- a simple integer (or singleton tuple) for a greyscale image.
-
- If specified, the `gamma` parameter must be a positive number
- (generally, a float). A ``gAMA`` chunk will be created. Note that
- this will not change the values of the pixels as they appear in
- the PNG file, they are assumed to have already been converted
- appropriately for the gamma specified.
-
- The `compression` argument specifies the compression level to
- be used by the ``zlib`` module. Values from 1 to 9 specify
- compression, with 9 being "more compressed" (usually smaller
- and slower, but it doesn't always work out that way). 0 means
- no compression. -1 and ``None`` both mean that the default
- level of compession will be picked by the ``zlib`` module
- (which is generally acceptable).
-
- If `interlace` is true then an interlaced image is created
- (using PNG's so far only interace method, *Adam7*). This does not
- affect how the pixels should be presented to the encoder, rather
- it changes how they are arranged into the PNG file. On slow
- connexions interlaced images can be partially decoded by the
- browser to give a rough view of the image that is successively
- refined as more image data appears.
-
- .. note ::
-
- Enabling the `interlace` option requires the entire image
- to be processed in working memory.
-
- `chunk_limit` is used to limit the amount of memory used whilst
- compressing the image. In order to avoid using large amounts of
- memory, multiple ``IDAT`` chunks may be created.
- """
-
- # At the moment the `planes` argument is ignored;
- # its purpose is to act as a dummy so that
- # ``Writer(x, y, **info)`` works, where `info` is a dictionary
- # returned by Reader.read and friends.
- # Ditto for `colormap`.
-
- # A couple of helper functions come first. Best skipped if you
- # are reading through.
-
- def isinteger(x):
- try:
- return int(x) == x
- except:
- return False
-
- def check_color(c, which):
- """Checks that a colour argument for transparent or
- background options is the right form. Also "corrects" bare
- integers to 1-tuples.
- """
-
- if c is None:
- return c
- if greyscale:
- try:
- l = len(c)
- except TypeError:
- c = (c,)
- if len(c) != 1:
- raise ValueError("%s for greyscale must be 1-tuple" %
- which)
- if not isinteger(c[0]):
- raise ValueError(
- "%s colour for greyscale must be integer" %
- which)
- else:
- if not (len(c) == 3 and
- isinteger(c[0]) and
- isinteger(c[1]) and
- isinteger(c[2])):
- raise ValueError(
- "%s colour must be a triple of integers" %
- which)
- return c
-
- if size:
- if len(size) != 2:
- raise ValueError(
- "size argument should be a pair (width, height)")
- if width is not None and width != size[0]:
- raise ValueError(
- "size[0] (%r) and width (%r) should match when both are used."
- % (size[0], width))
- if height is not None and height != size[1]:
- raise ValueError(
- "size[1] (%r) and height (%r) should match when both are used."
- % (size[1], height))
- width,height = size
- del size
-
- if width <= 0 or height <= 0:
- raise ValueError("width and height must be greater than zero")
- if not isinteger(width) or not isinteger(height):
- raise ValueError("width and height must be integers")
- # http://www.w3.org/TR/PNG/#7Integers-and-byte-order
- if width > 2**32-1 or height > 2**32-1:
- raise ValueError("width and height cannot exceed 2**32-1")
-
- if alpha and transparent is not None:
- raise ValueError(
- "transparent colour not allowed with alpha channel")
-
- if bytes_per_sample is not None:
- warnings.warn('please use bitdepth instead of bytes_per_sample',
- DeprecationWarning)
- if bytes_per_sample not in (0.125, 0.25, 0.5, 1, 2):
- raise ValueError(
- "bytes per sample must be .125, .25, .5, 1, or 2")
- bitdepth = int(8*bytes_per_sample)
- del bytes_per_sample
- if not isinteger(bitdepth) or bitdepth < 1 or 16 < bitdepth:
- raise ValueError("bitdepth (%r) must be a postive integer <= 16" %
- bitdepth)
-
- self.rescale = None
- if palette:
- if bitdepth not in (1,2,4,8):
- raise ValueError("with palette, bitdepth must be 1, 2, 4, or 8")
- if transparent is not None:
- raise ValueError("transparent and palette not compatible")
- if alpha:
- raise ValueError("alpha and palette not compatible")
- if greyscale:
- raise ValueError("greyscale and palette not compatible")
- else:
- # No palette, check for sBIT chunk generation.
- if alpha or not greyscale:
- if bitdepth not in (8,16):
- targetbitdepth = (8,16)[bitdepth > 8]
- self.rescale = (bitdepth, targetbitdepth)
- bitdepth = targetbitdepth
- del targetbitdepth
- else:
- assert greyscale
- assert not alpha
- if bitdepth not in (1,2,4,8,16):
- if bitdepth > 8:
- targetbitdepth = 16
- elif bitdepth == 3:
- targetbitdepth = 4
- else:
- assert bitdepth in (5,6,7)
- targetbitdepth = 8
- self.rescale = (bitdepth, targetbitdepth)
- bitdepth = targetbitdepth
- del targetbitdepth
-
- if bitdepth < 8 and (alpha or not greyscale and not palette):
- raise ValueError(
- "bitdepth < 8 only permitted with greyscale or palette")
- if bitdepth > 8 and palette:
- raise ValueError(
- "bit depth must be 8 or less for images with palette")
-
- transparent = check_color(transparent, 'transparent')
- background = check_color(background, 'background')
-
- # It's important that the true boolean values (greyscale, alpha,
- # colormap, interlace) are converted to bool because Iverson's
- # convention is relied upon later on.
- self.width = width
- self.height = height
- self.transparent = transparent
- self.background = background
- self.gamma = gamma
- self.greyscale = bool(greyscale)
- self.alpha = bool(alpha)
- self.colormap = bool(palette)
- self.bitdepth = int(bitdepth)
- self.compression = compression
- self.chunk_limit = chunk_limit
- self.interlace = bool(interlace)
- self.palette = check_palette(palette)
-
- self.color_type = 4*self.alpha + 2*(not greyscale) + 1*self.colormap
- assert self.color_type in (0,2,3,4,6)
-
- self.color_planes = (3,1)[self.greyscale or self.colormap]
- self.planes = self.color_planes + self.alpha
- # :todo: fix for bitdepth < 8
- self.psize = (self.bitdepth/8) * self.planes
-
- def make_palette(self):
- """Create the byte sequences for a ``PLTE`` and if necessary a
- ``tRNS`` chunk. Returned as a pair (*p*, *t*). *t* will be
- ``None`` if no ``tRNS`` chunk is necessary.
- """
-
- p = array('B')
- t = array('B')
-
- for x in self.palette:
- p.extend(x[0:3])
- if len(x) > 3:
- t.append(x[3])
- p = tostring(p)
- t = tostring(t)
- if t:
- return p,t
- return p,None
-
- def write(self, outfile, rows):
- """Write a PNG image to the output file. `rows` should be
- an iterable that yields each row in boxed row flat pixel format.
- The rows should be the rows of the original image, so there
- should be ``self.height`` rows of ``self.width * self.planes`` values.
- If `interlace` is specified (when creating the instance), then
- an interlaced PNG file will be written. Supply the rows in the
- normal image order; the interlacing is carried out internally.
-
- .. note ::
-
- Interlacing will require the entire image to be in working memory.
- """
-
- if self.interlace:
- fmt = 'BH'[self.bitdepth > 8]
- a = array(fmt, itertools.chain(*rows))
- return self.write_array(outfile, a)
- else:
- nrows = self.write_passes(outfile, rows)
- if nrows != self.height:
- raise ValueError(
- "rows supplied (%d) does not match height (%d)" %
- (nrows, self.height))
-
- def write_passes(self, outfile, rows, packed=False):
- """
- Write a PNG image to the output file.
-
- Most users are expected to find the :meth:`write` or
- :meth:`write_array` method more convenient.
-
- The rows should be given to this method in the order that
- they appear in the output file. For straightlaced images,
- this is the usual top to bottom ordering, but for interlaced
- images the rows should have already been interlaced before
- passing them to this function.
-
- `rows` should be an iterable that yields each row. When
- `packed` is ``False`` the rows should be in boxed row flat pixel
- format; when `packed` is ``True`` each row should be a packed
- sequence of bytes.
-
- """
-
- # http://www.w3.org/TR/PNG/#5PNG-file-signature
- outfile.write(_signature)
-
- # http://www.w3.org/TR/PNG/#11IHDR
- write_chunk(outfile, 'IHDR',
- struct.pack("!2I5B", self.width, self.height,
- self.bitdepth, self.color_type,
- 0, 0, self.interlace))
-
- # See :chunk:order
- # http://www.w3.org/TR/PNG/#11gAMA
- if self.gamma is not None:
- write_chunk(outfile, 'gAMA',
- struct.pack("!L", int(round(self.gamma*1e5))))
-
- # See :chunk:order
- # http://www.w3.org/TR/PNG/#11sBIT
- if self.rescale:
- write_chunk(outfile, 'sBIT',
- struct.pack('%dB' % self.planes,
- *[self.rescale[0]]*self.planes))
-
- # :chunk:order: Without a palette (PLTE chunk), ordering is
- # relatively relaxed. With one, gAMA chunk must precede PLTE
- # chunk which must precede tRNS and bKGD.
- # See http://www.w3.org/TR/PNG/#5ChunkOrdering
- if self.palette:
- p,t = self.make_palette()
- write_chunk(outfile, 'PLTE', p)
- if t:
- # tRNS chunk is optional. Only needed if palette entries
- # have alpha.
- write_chunk(outfile, 'tRNS', t)
-
- # http://www.w3.org/TR/PNG/#11tRNS
- if self.transparent is not None:
- if self.greyscale:
- write_chunk(outfile, 'tRNS',
- struct.pack("!1H", *self.transparent))
- else:
- write_chunk(outfile, 'tRNS',
- struct.pack("!3H", *self.transparent))
-
- # http://www.w3.org/TR/PNG/#11bKGD
- if self.background is not None:
- if self.greyscale:
- write_chunk(outfile, 'bKGD',
- struct.pack("!1H", *self.background))
- else:
- write_chunk(outfile, 'bKGD',
- struct.pack("!3H", *self.background))
-
- # http://www.w3.org/TR/PNG/#11IDAT
- if self.compression is not None:
- compressor = zlib.compressobj(self.compression)
- else:
- compressor = zlib.compressobj()
-
- # Choose an extend function based on the bitdepth. The extend
- # function packs/decomposes the pixel values into bytes and
- # stuffs them onto the data array.
- data = array('B')
- if self.bitdepth == 8 or packed:
- extend = data.extend
- elif self.bitdepth == 16:
- # Decompose into bytes
- def extend(sl):
- fmt = '!%dH' % len(sl)
- data.extend(array('B', struct.pack(fmt, *sl)))
- else:
- # Pack into bytes
- assert self.bitdepth < 8
- # samples per byte
- spb = int(8/self.bitdepth)
- def extend(sl):
- a = array('B', sl)
- # Adding padding bytes so we can group into a whole
- # number of spb-tuples.
- l = float(len(a))
- extra = math.ceil(l / float(spb))*spb - l
- a.extend([0]*int(extra))
- # Pack into bytes
- l = group(a, spb)
- l = [reduce(lambda x,y:
- (x << self.bitdepth) + y, e) for e in l]
- data.extend(l)
- if self.rescale:
- oldextend = extend
- factor = \
- float(2**self.rescale[1]-1) / float(2**self.rescale[0]-1)
- def extend(sl):
- oldextend([int(round(factor*x)) for x in sl])
-
- # Build the first row, testing mostly to see if we need to
- # changed the extend function to cope with NumPy integer types
- # (they cause our ordinary definition of extend to fail, so we
- # wrap it). See
- # http://code.google.com/p/pypng/issues/detail?id=44
- enumrows = enumerate(rows)
- del rows
-
- # First row's filter type.
- data.append(0)
- # :todo: Certain exceptions in the call to ``.next()`` or the
- # following try would indicate no row data supplied.
- # Should catch.
- i,row = next(enumrows)
- try:
- # If this fails...
- extend(row)
- except:
- # ... try a version that converts the values to int first.
- # Not only does this work for the (slightly broken) NumPy
- # types, there are probably lots of other, unknown, "nearly"
- # int types it works for.
- def wrapmapint(f):
- return lambda sl: f(list(map(int, sl)))
- extend = wrapmapint(extend)
- del wrapmapint
- extend(row)
-
- for i,row in enumrows:
- # Add "None" filter type. Currently, it's essential that
- # this filter type be used for every scanline as we do not
- # mark the first row of a reduced pass image; that means we
- # could accidentally compute the wrong filtered scanline if
- # we used "up", "average", or "paeth" on such a line.
- data.append(0)
- extend(row)
- if len(data) > self.chunk_limit:
- compressed = compressor.compress(tostring(data))
- if len(compressed):
- # print >> sys.stderr, len(data), len(compressed)
- write_chunk(outfile, 'IDAT', compressed)
- # Because of our very witty definition of ``extend``,
- # above, we must re-use the same ``data`` object. Hence
- # we use ``del`` to empty this one, rather than create a
- # fresh one (which would be my natural FP instinct).
- del data[:]
- if len(data):
- compressed = compressor.compress(tostring(data))
- else:
- compressed = ''
- flushed = compressor.flush()
- if len(compressed) or len(flushed):
- # print >> sys.stderr, len(data), len(compressed), len(flushed)
- write_chunk(outfile, 'IDAT', compressed + flushed)
- # http://www.w3.org/TR/PNG/#11IEND
- write_chunk(outfile, 'IEND')
- return i+1
-
- def write_array(self, outfile, pixels):
- """
- Write an array in flat row flat pixel format as a PNG file on
- the output file. See also :meth:`write` method.
- """
-
- if self.interlace:
- self.write_passes(outfile, self.array_scanlines_interlace(pixels))
- else:
- self.write_passes(outfile, self.array_scanlines(pixels))
-
- def write_packed(self, outfile, rows):
- """
- Write PNG file to `outfile`. The pixel data comes from `rows`
- which should be in boxed row packed format. Each row should be
- a sequence of packed bytes.
-
- Technically, this method does work for interlaced images but it
- is best avoided. For interlaced images, the rows should be
- presented in the order that they appear in the file.
-
- This method should not be used when the source image bit depth
- is not one naturally supported by PNG; the bit depth should be
- 1, 2, 4, 8, or 16.
- """
-
- if self.rescale:
- raise Error("write_packed method not suitable for bit depth %d" %
- self.rescale[0])
- return self.write_passes(outfile, rows, packed=True)
-
- def convert_pnm(self, infile, outfile):
- """
- Convert a PNM file containing raw pixel data into a PNG file
- with the parameters set in the writer object. Works for
- (binary) PGM, PPM, and PAM formats.
- """
-
- if self.interlace:
- pixels = array('B')
- pixels.fromfile(infile,
- (self.bitdepth/8) * self.color_planes *
- self.width * self.height)
- self.write_passes(outfile, self.array_scanlines_interlace(pixels))
- else:
- self.write_passes(outfile, self.file_scanlines(infile))
-
- def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile):
- """
- Convert a PPM and PGM file containing raw pixel data into a
- PNG outfile with the parameters set in the writer object.
- """
- pixels = array('B')
- pixels.fromfile(ppmfile,
- (self.bitdepth/8) * self.color_planes *
- self.width * self.height)
- apixels = array('B')
- apixels.fromfile(pgmfile,
- (self.bitdepth/8) *
- self.width * self.height)
- pixels = interleave_planes(pixels, apixels,
- (self.bitdepth/8) * self.color_planes,
- (self.bitdepth/8))
- if self.interlace:
- self.write_passes(outfile, self.array_scanlines_interlace(pixels))
- else:
- self.write_passes(outfile, self.array_scanlines(pixels))
-
- def file_scanlines(self, infile):
- """
- Generates boxed rows in flat pixel format, from the input file
- `infile`. It assumes that the input file is in a "Netpbm-like"
- binary format, and is positioned at the beginning of the first
- pixel. The number of pixels to read is taken from the image
- dimensions (`width`, `height`, `planes`) and the number of bytes
- per value is implied by the image `bitdepth`.
- """
-
- # Values per row
- vpr = self.width * self.planes
- row_bytes = vpr
- if self.bitdepth > 8:
- assert self.bitdepth == 16
- row_bytes *= 2
- fmt = '>%dH' % vpr
- def line():
- return array('H', struct.unpack(fmt, infile.read(row_bytes)))
- else:
- def line():
- scanline = array('B', infile.read(row_bytes))
- return scanline
- for y in range(self.height):
- yield line()
-
- def array_scanlines(self, pixels):
- """
- Generates boxed rows (flat pixels) from flat rows (flat pixels)
- in an array.
- """
-
- # Values per row
- vpr = self.width * self.planes
- stop = 0
- for y in range(self.height):
- start = stop
- stop = start + vpr
- yield pixels[start:stop]
-
- def array_scanlines_interlace(self, pixels):
- """
- Generator for interlaced scanlines from an array. `pixels` is
- the full source image in flat row flat pixel format. The
- generator yields each scanline of the reduced passes in turn, in
- boxed row flat pixel format.
- """
-
- # http://www.w3.org/TR/PNG/#8InterlaceMethods
- # Array type.
- fmt = 'BH'[self.bitdepth > 8]
- # Value per row
- vpr = self.width * self.planes
- for xstart, ystart, xstep, ystep in _adam7:
- if xstart >= self.width:
- continue
- # Pixels per row (of reduced image)
- ppr = int(math.ceil((self.width-xstart)/float(xstep)))
- # number of values in reduced image row.
- row_len = ppr*self.planes
- for y in range(ystart, self.height, ystep):
- if xstep == 1:
- offset = y * vpr
- yield pixels[offset:offset+vpr]
- else:
- row = array(fmt)
- # There's no easier way to set the length of an array
- row.extend(pixels[0:row_len])
- offset = y * vpr + xstart * self.planes
- end_offset = (y+1) * vpr
- skip = self.planes * xstep
- for i in range(self.planes):
- row[i::self.planes] = \
- pixels[offset+i:end_offset:skip]
- yield row
-
-def write_chunk(outfile, tag, data=strtobytes('')):
- """
- Write a PNG chunk to the output file, including length and
- checksum.
- """
-
- # http://www.w3.org/TR/PNG/#5Chunk-layout
- outfile.write(struct.pack("!I", len(data)))
- tag = strtobytes(tag)
- outfile.write(tag)
- outfile.write(data)
- checksum = zlib.crc32(tag)
- checksum = zlib.crc32(data, checksum)
- checksum &= 2**32-1
- outfile.write(struct.pack("!I", checksum))
-
-def write_chunks(out, chunks):
- """Create a PNG file by writing out the chunks."""
-
- out.write(_signature)
- for chunk in chunks:
- write_chunk(out, *chunk)
-
-def filter_scanline(type, line, fo, prev=None):
- """Apply a scanline filter to a scanline. `type` specifies the
- filter type (0 to 4); `line` specifies the current (unfiltered)
- scanline as a sequence of bytes; `prev` specifies the previous
- (unfiltered) scanline as a sequence of bytes. `fo` specifies the
- filter offset; normally this is size of a pixel in bytes (the number
- of bytes per sample times the number of channels), but when this is
- < 1 (for bit depths < 8) then the filter offset is 1.
- """
-
- assert 0 <= type < 5
-
- # The output array. Which, pathetically, we extend one-byte at a
- # time (fortunately this is linear).
- out = array('B', [type])
-
- def sub():
- ai = -fo
- for x in line:
- if ai >= 0:
- x = (x - line[ai]) & 0xff
- out.append(x)
- ai += 1
- def up():
- for i,x in enumerate(line):
- x = (x - prev[i]) & 0xff
- out.append(x)
- def average():
- ai = -fo
- for i,x in enumerate(line):
- if ai >= 0:
- x = (x - ((line[ai] + prev[i]) >> 1)) & 0xff
- else:
- x = (x - (prev[i] >> 1)) & 0xff
- out.append(x)
- ai += 1
- def paeth():
- # http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth
- ai = -fo # also used for ci
- for i,x in enumerate(line):
- a = 0
- b = prev[i]
- c = 0
-
- if ai >= 0:
- a = line[ai]
- c = prev[ai]
- p = a + b - c
- pa = abs(p - a)
- pb = abs(p - b)
- pc = abs(p - c)
- if pa <= pb and pa <= pc: Pr = a
- elif pb <= pc: Pr = b
- else: Pr = c
-
- x = (x - Pr) & 0xff
- out.append(x)
- ai += 1
-
- if not prev:
- # We're on the first line. Some of the filters can be reduced
- # to simpler cases which makes handling the line "off the top"
- # of the image simpler. "up" becomes "none"; "paeth" becomes
- # "left" (non-trivial, but true). "average" needs to be handled
- # specially.
- if type == 2: # "up"
- type = 0
- elif type == 3:
- prev = [0]*len(line)
- elif type == 4: # "paeth"
- type = 1
- if type == 0:
- out.extend(line)
- elif type == 1:
- sub()
- elif type == 2:
- up()
- elif type == 3:
- average()
- else: # type == 4
- paeth()
- return out
-
-
-def from_array(a, mode=None, info={}):
- """Create a PNG :class:`Image` object from a 2- or 3-dimensional array.
- One application of this function is easy PIL-style saving:
- ``png.from_array(pixels, 'L').save('foo.png')``.
-
- .. note :
-
- The use of the term *3-dimensional* is for marketing purposes
- only. It doesn't actually work. Please bear with us. Meanwhile
- enjoy the complimentary snacks (on request) and please use a
- 2-dimensional array.
-
- Unless they are specified using the *info* parameter, the PNG's
- height and width are taken from the array size. For a 3 dimensional
- array the first axis is the height; the second axis is the width;
- and the third axis is the channel number. Thus an RGB image that is
- 16 pixels high and 8 wide will use an array that is 16x8x3. For 2
- dimensional arrays the first axis is the height, but the second axis
- is ``width*channels``, so an RGB image that is 16 pixels high and 8
- wide will use a 2-dimensional array that is 16x24 (each row will be
- 8*3==24 sample values).
-
- *mode* is a string that specifies the image colour format in a
- PIL-style mode. It can be:
-
- ``'L'``
- greyscale (1 channel)
- ``'LA'``
- greyscale with alpha (2 channel)
- ``'RGB'``
- colour image (3 channel)
- ``'RGBA'``
- colour image with alpha (4 channel)
-
- The mode string can also specify the bit depth (overriding how this
- function normally derives the bit depth, see below). Appending
- ``';16'`` to the mode will cause the PNG to be 16 bits per channel;
- any decimal from 1 to 16 can be used to specify the bit depth.
-
- When a 2-dimensional array is used *mode* determines how many
- channels the image has, and so allows the width to be derived from
- the second array dimension.
-
- The array is expected to be a ``numpy`` array, but it can be any
- suitable Python sequence. For example, a list of lists can be used:
- ``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``. The exact
- rules are: ``len(a)`` gives the first dimension, height;
- ``len(a[0])`` gives the second dimension; ``len(a[0][0])`` gives the
- third dimension, unless an exception is raised in which case a
- 2-dimensional array is assumed. It's slightly more complicated than
- that because an iterator of rows can be used, and it all still
- works. Using an iterator allows data to be streamed efficiently.
-
- The bit depth of the PNG is normally taken from the array element's
- datatype (but if *mode* specifies a bitdepth then that is used
- instead). The array element's datatype is determined in a way which
- is supposed to work both for ``numpy`` arrays and for Python
- ``array.array`` objects. A 1 byte datatype will give a bit depth of
- 8, a 2 byte datatype will give a bit depth of 16. If the datatype
- does not have an implicit size, for example it is a plain Python
- list of lists, as above, then a default of 8 is used.
-
- The *info* parameter is a dictionary that can be used to specify
- metadata (in the same style as the arguments to the
- :class:``png.Writer`` class). For this function the keys that are
- useful are:
-
- height
- overrides the height derived from the array dimensions and allows
- *a* to be an iterable.
- width
- overrides the width derived from the array dimensions.
- bitdepth
- overrides the bit depth derived from the element datatype (but
- must match *mode* if that also specifies a bit depth).
-
- Generally anything specified in the
- *info* dictionary will override any implicit choices that this
- function would otherwise make, but must match any explicit ones.
- For example, if the *info* dictionary has a ``greyscale`` key then
- this must be true when mode is ``'L'`` or ``'LA'`` and false when
- mode is ``'RGB'`` or ``'RGBA'``.
- """
-
- # We abuse the *info* parameter by modifying it. Take a copy here.
- # (Also typechecks *info* to some extent).
- info = dict(info)
-
- # Syntax check mode string.
- bitdepth = None
- try:
- mode = mode.split(';')
- if len(mode) not in (1,2):
- raise Error()
- if mode[0] not in ('L', 'LA', 'RGB', 'RGBA'):
- raise Error()
- if len(mode) == 2:
- try:
- bitdepth = int(mode[1])
- except:
- raise Error()
- except Error:
- raise Error("mode string should be 'RGB' or 'L;16' or similar.")
- mode = mode[0]
-
- # Get bitdepth from *mode* if possible.
- if bitdepth:
- if info.get('bitdepth') and bitdepth != info['bitdepth']:
- raise Error("mode bitdepth (%d) should match info bitdepth (%d)." %
- (bitdepth, info['bitdepth']))
- info['bitdepth'] = bitdepth
-
- # Fill in and/or check entries in *info*.
- # Dimensions.
- if 'size' in info:
- # Check width, height, size all match where used.
- for dimension,axis in [('width', 0), ('height', 1)]:
- if dimension in info:
- if info[dimension] != info['size'][axis]:
- raise Error(
- "info[%r] shhould match info['size'][%r]." %
- (dimension, axis))
- info['width'],info['height'] = info['size']
- if 'height' not in info:
- try:
- l = len(a)
- except:
- raise Error(
- "len(a) does not work, supply info['height'] instead.")
- info['height'] = l
- # Colour format.
- if 'greyscale' in info:
- if bool(info['greyscale']) != ('L' in mode):
- raise Error("info['greyscale'] should match mode.")
- info['greyscale'] = 'L' in mode
- if 'alpha' in info:
- if bool(info['alpha']) != ('A' in mode):
- raise Error("info['alpha'] should match mode.")
- info['alpha'] = 'A' in mode
-
- planes = len(mode)
- if 'planes' in info:
- if info['planes'] != planes:
- raise Error("info['planes'] should match mode.")
-
- # In order to work out whether we the array is 2D or 3D we need its
- # first row, which requires that we take a copy of its iterator.
- # We may also need the first row to derive width and bitdepth.
- a,t = itertools.tee(a)
- row = next(t)
- del t
- try:
- row[0][0]
- threed = True
- testelement = row[0]
- except:
- threed = False
- testelement = row
- if 'width' not in info:
- if threed:
- width = len(row)
- else:
- width = len(row) // planes
- info['width'] = width
-
- # Not implemented yet
- assert not threed
-
- if 'bitdepth' not in info:
- try:
- dtype = testelement.dtype
- # goto the "else:" clause. Sorry.
- except:
- try:
- # Try a Python array.array.
- bitdepth = 8 * testelement.itemsize
- except:
- # We can't determine it from the array element's
- # datatype, use a default of 8.
- bitdepth = 8
- else:
- # If we got here without exception, we now assume that
- # the array is a numpy array.
- if dtype.kind == 'b':
- bitdepth = 1
- else:
- bitdepth = 8 * dtype.itemsize
- info['bitdepth'] = bitdepth
-
- for thing in 'width height bitdepth greyscale alpha'.split():
- assert thing in info
- return Image(a, info)
-
-# So that refugee's from PIL feel more at home. Not documented.
-fromarray = from_array
-
-class Image:
- """A PNG image.
- You can create an :class:`Image` object from an array of pixels by calling
- :meth:`png.from_array`. It can be saved to disk with the
- :meth:`save` method."""
- def __init__(self, rows, info):
- """
- .. note ::
-
- The constructor is not public. Please do not call it.
- """
-
- self.rows = rows
- self.info = info
-
- def save(self, file):
- """Save the image to *file*. If *file* looks like an open file
- descriptor then it is used, otherwise it is treated as a
- filename and a fresh file is opened.
-
- In general, you can only call this method once; after it has
- been called the first time and the PNG image has been saved, the
- source data will have been streamed, and cannot be streamed
- again.
- """
-
- w = Writer(**self.info)
-
- try:
- file.write
- def close(): pass
- except:
- file = open(file, 'wb')
- def close(): file.close()
-
- try:
- w.write(file, self.rows)
- finally:
- close()
-
-class _readable:
- """
- A simple file-like interface for strings and arrays.
- """
-
- def __init__(self, buf):
- self.buf = buf
- self.offset = 0
-
- def read(self, n):
- r = self.buf[self.offset:self.offset+n]
- if isarray(r):
- r = r.tostring()
- self.offset += n
- return r
-
-
-class Reader:
- """
- PNG decoder in pure Python.
- """
-
- def __init__(self, _guess=None, **kw):
- """
- Create a PNG decoder object.
-
- The constructor expects exactly one keyword argument. If you
- supply a positional argument instead, it will guess the input
- type. You can choose among the following keyword arguments:
-
- filename
- Name of input file (a PNG file).
- file
- A file-like object (object with a read() method).
- bytes
- ``array`` or ``string`` with PNG data.
-
- """
- if ((_guess is not None and len(kw) != 0) or
- (_guess is None and len(kw) != 1)):
- raise TypeError("Reader() takes exactly 1 argument")
-
- # Will be the first 8 bytes, later on. See validate_signature.
- self.signature = None
- self.transparent = None
- # A pair of (len,type) if a chunk has been read but its data and
- # checksum have not (in other words the file position is just
- # past the 4 bytes that specify the chunk type). See preamble
- # method for how this is used.
- self.atchunk = None
-
- if _guess is not None:
- if isarray(_guess):
- kw["bytes"] = _guess
- elif isinstance(_guess, str):
- kw["filename"] = _guess
- elif hasattr(_guess, 'read'):
- kw["file"] = _guess
-
- if "filename" in kw:
- self.file = open(kw["filename"], "rb")
- elif "file" in kw:
- self.file = kw["file"]
- elif "bytes" in kw:
- self.file = _readable(kw["bytes"])
- else:
- raise TypeError("expecting filename, file or bytes array")
-
-
- def chunk(self, seek=None, lenient=False):
- """
- Read the next PNG chunk from the input file; returns a
- (*type*,*data*) tuple. *type* is the chunk's type as a string
- (all PNG chunk types are 4 characters long). *data* is the
- chunk's data content, as a string.
-
- If the optional `seek` argument is
- specified then it will keep reading chunks until it either runs
- out of file or finds the type specified by the argument. Note
- that in general the order of chunks in PNGs is unspecified, so
- using `seek` can cause you to miss chunks.
-
- If the optional `lenient` argument evaluates to True,
- checksum failures will raise warnings rather than exceptions.
- """
-
- self.validate_signature()
-
- while True:
- # http://www.w3.org/TR/PNG/#5Chunk-layout
- if not self.atchunk:
- self.atchunk = self.chunklentype()
- length,type = self.atchunk
- self.atchunk = None
- data = self.file.read(length)
- if len(data) != length:
- raise ChunkError('Chunk %s too short for required %i octets.'
- % (type, length))
- checksum = self.file.read(4)
- if len(checksum) != 4:
- raise ValueError('Chunk %s too short for checksum.', tag)
- if seek and type != seek:
- continue
- verify = zlib.crc32(strtobytes(type))
- verify = zlib.crc32(data, verify)
- # Whether the output from zlib.crc32 is signed or not varies
- # according to hideous implementation details, see
- # http://bugs.python.org/issue1202 .
- # We coerce it to be positive here (in a way which works on
- # Python 2.3 and older).
- verify &= 2**32 - 1
- verify = struct.pack('!I', verify)
- if checksum != verify:
- # print repr(checksum)
- (a, ) = struct.unpack('!I', checksum)
- (b, ) = struct.unpack('!I', verify)
- message = "Checksum error in %s chunk: 0x%08X != 0x%08X." % (type, a, b)
- if lenient:
- warnings.warn(message, RuntimeWarning)
- else:
- raise ChunkError(message)
- return type, data
-
- def chunks(self):
- """Return an iterator that will yield each chunk as a
- (*chunktype*, *content*) pair.
- """
-
- while True:
- t,v = self.chunk()
- yield t,v
- if t == 'IEND':
- break
-
- def undo_filter(self, filter_type, scanline, previous):
- """Undo the filter for a scanline. `scanline` is a sequence of
- bytes that does not include the initial filter type byte.
- `previous` is decoded previous scanline (for straightlaced
- images this is the previous pixel row, but for interlaced
- images, it is the previous scanline in the reduced image, which
- in general is not the previous pixel row in the final image).
- When there is no previous scanline (the first row of a
- straightlaced image, or the first row in one of the passes in an
- interlaced image), then this argument should be ``None``.
-
- The scanline will have the effects of filtering removed, and the
- result will be returned as a fresh sequence of bytes.
- """
-
- # :todo: Would it be better to update scanline in place?
- # Yes, with the Cython extension making the undo_filter fast,
- # updating scanline inplace makes the code 3 times faster
- # (reading 50 images of 800x800 went from 40s to 16s)
- result = scanline
-
- if filter_type == 0:
- return result
-
- if filter_type not in (1,2,3,4):
- raise FormatError('Invalid PNG Filter Type.'
- ' See http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters .')
-
- # Filter unit. The stride from one pixel to the corresponding
- # byte from the previous previous. Normally this is the pixel
- # size in bytes, but when this is smaller than 1, the previous
- # byte is used instead.
- fu = max(1, self.psize)
-
- # For the first line of a pass, synthesize a dummy previous
- # line. An alternative approach would be to observe that on the
- # first line 'up' is the same as 'null', 'paeth' is the same
- # as 'sub', with only 'average' requiring any special case.
- if not previous:
- previous = array('B', [0]*len(scanline))
-
- def sub():
- """Undo sub filter."""
-
- ai = 0
- # Loops starts at index fu. Observe that the initial part
- # of the result is already filled in correctly with
- # scanline.
- for i in range(fu, len(result)):
- x = scanline[i]
- a = result[ai]
- result[i] = (x + a) & 0xff
- ai += 1
-
- def up():
- """Undo up filter."""
-
- for i in range(len(result)):
- x = scanline[i]
- b = previous[i]
- result[i] = (x + b) & 0xff
-
- def average():
- """Undo average filter."""
-
- ai = -fu
- for i in range(len(result)):
- x = scanline[i]
- if ai < 0:
- a = 0
- else:
- a = result[ai]
- b = previous[i]
- result[i] = (x + ((a + b) >> 1)) & 0xff
- ai += 1
-
- def paeth():
- """Undo Paeth filter."""
-
- # Also used for ci.
- ai = -fu
- for i in range(len(result)):
- x = scanline[i]
- if ai < 0:
- a = c = 0
- else:
- a = result[ai]
- c = previous[ai]
- b = previous[i]
- p = a + b - c
- pa = abs(p - a)
- pb = abs(p - b)
- pc = abs(p - c)
- if pa <= pb and pa <= pc:
- pr = a
- elif pb <= pc:
- pr = b
- else:
- pr = c
- result[i] = (x + pr) & 0xff
- ai += 1
-
- # Call appropriate filter algorithm. Note that 0 has already
- # been dealt with.
- (None,
- pngfilters.undo_filter_sub,
- pngfilters.undo_filter_up,
- pngfilters.undo_filter_average,
- pngfilters.undo_filter_paeth)[filter_type](fu, scanline, previous, result)
- return result
-
- def deinterlace(self, raw):
- """
- Read raw pixel data, undo filters, deinterlace, and flatten.
- Return in flat row flat pixel format.
- """
-
- # print >> sys.stderr, ("Reading interlaced, w=%s, r=%s, planes=%s," +
- # " bpp=%s") % (self.width, self.height, self.planes, self.bps)
- # Values per row (of the target image)
- vpr = self.width * self.planes
-
- # Make a result array, and make it big enough. Interleaving
- # writes to the output array randomly (well, not quite), so the
- # entire output array must be in memory.
- fmt = 'BH'[self.bitdepth > 8]
- a = array(fmt, [0]*vpr*self.height)
- source_offset = 0
-
- for xstart, ystart, xstep, ystep in _adam7:
- # print >> sys.stderr, "Adam7: start=%s,%s step=%s,%s" % (
- # xstart, ystart, xstep, ystep)
- if xstart >= self.width:
- continue
- # The previous (reconstructed) scanline. None at the
- # beginning of a pass to indicate that there is no previous
- # line.
- recon = None
- # Pixels per row (reduced pass image)
- ppr = int(math.ceil((self.width-xstart)/float(xstep)))
- # Row size in bytes for this pass.
- row_size = int(math.ceil(self.psize * ppr))
- for y in range(ystart, self.height, ystep):
- filter_type = raw[source_offset]
- source_offset += 1
- scanline = raw[source_offset:source_offset+row_size]
- source_offset += row_size
- recon = self.undo_filter(filter_type, scanline, recon)
- # Convert so that there is one element per pixel value
- flat = self.serialtoflat(recon, ppr)
- if xstep == 1:
- assert xstart == 0
- offset = y * vpr
- a[offset:offset+vpr] = flat
- else:
- offset = y * vpr + xstart * self.planes
- end_offset = (y+1) * vpr
- skip = self.planes * xstep
- for i in range(self.planes):
- a[offset+i:end_offset:skip] = \
- flat[i::self.planes]
- return a
-
- def iterboxed(self, rows):
- """Iterator that yields each scanline in boxed row flat pixel
- format. `rows` should be an iterator that yields the bytes of
- each row in turn.
- """
-
- def asvalues(raw):
- """Convert a row of raw bytes into a flat row. Result may
- or may not share with argument"""
-
- if self.bitdepth == 8:
- return raw
- if self.bitdepth == 16:
- raw = tostring(raw)
- return array('H', struct.unpack('!%dH' % (len(raw)//2), raw))
- assert self.bitdepth < 8
- width = self.width
- # Samples per byte
- spb = 8//self.bitdepth
- out = array('B')
- mask = 2**self.bitdepth - 1
- shifts = list(map(self.bitdepth.__mul__, reversed(list(range(spb)))))
- for o in raw:
- out.extend([mask&(o>>i) for i in shifts])
- return out[:width]
-
- return map(asvalues, rows)
-
- def serialtoflat(self, bytes, width=None):
- """Convert serial format (byte stream) pixel data to flat row
- flat pixel.
- """
-
- if self.bitdepth == 8:
- return bytes
- if self.bitdepth == 16:
- bytes = tostring(bytes)
- return array('H',
- struct.unpack('!%dH' % (len(bytes)//2), bytes))
- assert self.bitdepth < 8
- if width is None:
- width = self.width
- # Samples per byte
- spb = 8//self.bitdepth
- out = array('B')
- mask = 2**self.bitdepth - 1
- shifts = list(map(self.bitdepth.__mul__, reversed(list(range(spb)))))
- l = width
- for o in bytes:
- out.extend([(mask&(o>>s)) for s in shifts][:l])
- l -= spb
- if l <= 0:
- l = width
- return out
-
- def iterstraight(self, raw):
- """Iterator that undoes the effect of filtering, and yields each
- row in serialised format (as a sequence of bytes). Assumes input
- is straightlaced. `raw` should be an iterable that yields the
- raw bytes in chunks of arbitrary size."""
-
- # length of row, in bytes
- rb = self.row_bytes
- a = array('B')
- # The previous (reconstructed) scanline. None indicates first
- # line of image.
- recon = None
- for some in raw:
- a.extend(some)
- while len(a) >= rb + 1:
- filter_type = a[0]
- scanline = a[1:rb+1]
- del a[:rb+1]
- recon = self.undo_filter(filter_type, scanline, recon)
- yield recon
- if len(a) != 0:
- # :file:format We get here with a file format error: when the
- # available bytes (after decompressing) do not pack into exact
- # rows.
- raise FormatError(
- 'Wrong size for decompressed IDAT chunk.')
- assert len(a) == 0
-
- def validate_signature(self):
- """If signature (header) has not been read then read and
- validate it; otherwise do nothing.
- """
-
- if self.signature:
- return
- self.signature = self.file.read(8)
- if self.signature != _signature:
- raise FormatError("PNG file has invalid signature.")
-
- def preamble(self, lenient=False):
- """
- Extract the image metadata by reading the initial part of the PNG
- file up to the start of the ``IDAT`` chunk. All the chunks that
- precede the ``IDAT`` chunk are read and either processed for
- metadata or discarded.
-
- If the optional `lenient` argument evaluates to True,
- checksum failures will raise warnings rather than exceptions.
- """
-
- self.validate_signature()
-
- while True:
- if not self.atchunk:
- self.atchunk = self.chunklentype()
- if self.atchunk is None:
- raise FormatError(
- 'This PNG file has no IDAT chunks.')
- if self.atchunk[1] == 'IDAT':
- return
- self.process_chunk(lenient=lenient)
-
- def chunklentype(self):
- """Reads just enough of the input to determine the next
- chunk's length and type, returned as a (*length*, *type*) pair
- where *type* is a string. If there are no more chunks, ``None``
- is returned.
- """
-
- x = self.file.read(8)
- if not x:
- return None
- if len(x) != 8:
- raise FormatError(
- 'End of file whilst reading chunk length and type.')
- length,type = struct.unpack('!I4s', x)
- type = bytestostr(type)
- if length > 2**31-1:
- raise FormatError('Chunk %s is too large: %d.' % (type,length))
- return length,type
-
- def process_chunk(self, lenient=False):
- """Process the next chunk and its data. This only processes the
- following chunk types, all others are ignored: ``IHDR``,
- ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``.
-
- If the optional `lenient` argument evaluates to True,
- checksum failures will raise warnings rather than exceptions.
- """
-
- type, data = self.chunk(lenient=lenient)
- if type == 'IHDR':
- # http://www.w3.org/TR/PNG/#11IHDR
- if len(data) != 13:
- raise FormatError('IHDR chunk has incorrect length.')
- (self.width, self.height, self.bitdepth, self.color_type,
- self.compression, self.filter,
- self.interlace) = struct.unpack("!2I5B", data)
-
- # Check that the header specifies only valid combinations.
- if self.bitdepth not in (1,2,4,8,16):
- raise Error("invalid bit depth %d" % self.bitdepth)
- if self.color_type not in (0,2,3,4,6):
- raise Error("invalid colour type %d" % self.color_type)
- # Check indexed (palettized) images have 8 or fewer bits
- # per pixel; check only indexed or greyscale images have
- # fewer than 8 bits per pixel.
- if ((self.color_type & 1 and self.bitdepth > 8) or
- (self.bitdepth < 8 and self.color_type not in (0,3))):
- raise FormatError("Illegal combination of bit depth (%d)"
- " and colour type (%d)."
- " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ."
- % (self.bitdepth, self.color_type))
- if self.compression != 0:
- raise Error("unknown compression method %d" % self.compression)
- if self.filter != 0:
- raise FormatError("Unknown filter method %d,"
- " see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ."
- % self.filter)
- if self.interlace not in (0,1):
- raise FormatError("Unknown interlace method %d,"
- " see http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods ."
- % self.interlace)
-
- # Derived values
- # http://www.w3.org/TR/PNG/#6Colour-values
- colormap = bool(self.color_type & 1)
- greyscale = not (self.color_type & 2)
- alpha = bool(self.color_type & 4)
- color_planes = (3,1)[greyscale or colormap]
- planes = color_planes + alpha
-
- self.colormap = colormap
- self.greyscale = greyscale
- self.alpha = alpha
- self.color_planes = color_planes
- self.planes = planes
- self.psize = float(self.bitdepth)/float(8) * planes
- if int(self.psize) == self.psize:
- self.psize = int(self.psize)
- self.row_bytes = int(math.ceil(self.width * self.psize))
- # Stores PLTE chunk if present, and is used to check
- # chunk ordering constraints.
- self.plte = None
- # Stores tRNS chunk if present, and is used to check chunk
- # ordering constraints.
- self.trns = None
- # Stores sbit chunk if present.
- self.sbit = None
- elif type == 'PLTE':
- # http://www.w3.org/TR/PNG/#11PLTE
- if self.plte:
- warnings.warn("Multiple PLTE chunks present.")
- self.plte = data
- if len(data) % 3 != 0:
- raise FormatError(
- "PLTE chunk's length should be a multiple of 3.")
- if len(data) > (2**self.bitdepth)*3:
- raise FormatError("PLTE chunk is too long.")
- if len(data) == 0:
- raise FormatError("Empty PLTE is not allowed.")
- elif type == 'bKGD':
- try:
- if self.colormap:
- if not self.plte:
- warnings.warn(
- "PLTE chunk is required before bKGD chunk.")
- self.background = struct.unpack('B', data)
- else:
- self.background = struct.unpack("!%dH" % self.color_planes,
- data)
- except struct.error:
- raise FormatError("bKGD chunk has incorrect length.")
- elif type == 'tRNS':
- # http://www.w3.org/TR/PNG/#11tRNS
- self.trns = data
- if self.colormap:
- if not self.plte:
- warnings.warn("PLTE chunk is required before tRNS chunk.")
- else:
- if len(data) > len(self.plte)/3:
- # Was warning, but promoted to Error as it
- # would otherwise cause pain later on.
- raise FormatError("tRNS chunk is too long.")
- else:
- if self.alpha:
- raise FormatError(
- "tRNS chunk is not valid with colour type %d." %
- self.color_type)
- try:
- self.transparent = \
- struct.unpack("!%dH" % self.color_planes, data)
- except struct.error:
- raise FormatError("tRNS chunk has incorrect length.")
- elif type == 'gAMA':
- try:
- self.gamma = struct.unpack("!L", data)[0] / 100000.0
- except struct.error:
- raise FormatError("gAMA chunk has incorrect length.")
- elif type == 'sBIT':
- self.sbit = data
- if (self.colormap and len(data) != 3 or
- not self.colormap and len(data) != self.planes):
- raise FormatError("sBIT chunk has incorrect length.")
-
- def read(self, lenient=False):
- """
- Read the PNG file and decode it. Returns (`width`, `height`,
- `pixels`, `metadata`).
-
- May use excessive memory.
-
- `pixels` are returned in boxed row flat pixel format.
-
- If the optional `lenient` argument evaluates to True,
- checksum failures will raise warnings rather than exceptions.
- """
-
- def iteridat():
- """Iterator that yields all the ``IDAT`` chunks as strings."""
- while True:
- try:
- type, data = self.chunk(lenient=lenient)
- except ValueError as e:
- raise ChunkError(e.args[0])
- if type == 'IEND':
- # http://www.w3.org/TR/PNG/#11IEND
- break
- if type != 'IDAT':
- continue
- # type == 'IDAT'
- # http://www.w3.org/TR/PNG/#11IDAT
- if self.colormap and not self.plte:
- warnings.warn("PLTE chunk is required before IDAT chunk")
- yield data
-
- def iterdecomp(idat):
- """Iterator that yields decompressed strings. `idat` should
- be an iterator that yields the ``IDAT`` chunk data.
- """
-
- # Currently, with no max_length paramter to decompress, this
- # routine will do one yield per IDAT chunk. So not very
- # incremental.
- d = zlib.decompressobj()
- # Each IDAT chunk is passed to the decompressor, then any
- # remaining state is decompressed out.
- for data in idat:
- # :todo: add a max_length argument here to limit output
- # size.
- yield array('B', d.decompress(data))
- yield array('B', d.flush())
-
- self.preamble(lenient=lenient)
- raw = iterdecomp(iteridat())
-
- if self.interlace:
- raw = array('B', itertools.chain(*raw))
- arraycode = 'BH'[self.bitdepth>8]
- # Like :meth:`group` but producing an array.array object for
- # each row.
- pixels = map(lambda *row: array(arraycode, row),
- *[iter(self.deinterlace(raw))]*self.width*self.planes)
- else:
- pixels = self.iterboxed(self.iterstraight(raw))
- meta = dict()
- for attr in 'greyscale alpha planes bitdepth interlace'.split():
- meta[attr] = getattr(self, attr)
- meta['size'] = (self.width, self.height)
- for attr in 'gamma transparent background'.split():
- a = getattr(self, attr, None)
- if a is not None:
- meta[attr] = a
- if self.plte:
- meta['palette'] = self.palette()
- return self.width, self.height, pixels, meta
-
-
- def read_flat(self):
- """
- Read a PNG file and decode it into flat row flat pixel format.
- Returns (*width*, *height*, *pixels*, *metadata*).
-
- May use excessive memory.
-
- `pixels` are returned in flat row flat pixel format.
-
- See also the :meth:`read` method which returns pixels in the
- more stream-friendly boxed row flat pixel format.
- """
-
- x, y, pixel, meta = self.read()
- arraycode = 'BH'[meta['bitdepth']>8]
- pixel = array(arraycode, itertools.chain(*pixel))
- return x, y, pixel, meta
-
- def palette(self, alpha='natural'):
- """Returns a palette that is a sequence of 3-tuples or 4-tuples,
- synthesizing it from the ``PLTE`` and ``tRNS`` chunks. These
- chunks should have already been processed (for example, by
- calling the :meth:`preamble` method). All the tuples are the
- same size: 3-tuples if there is no ``tRNS`` chunk, 4-tuples when
- there is a ``tRNS`` chunk. Assumes that the image is colour type
- 3 and therefore a ``PLTE`` chunk is required.
-
- If the `alpha` argument is ``'force'`` then an alpha channel is
- always added, forcing the result to be a sequence of 4-tuples.
- """
-
- if not self.plte:
- raise FormatError(
- "Required PLTE chunk is missing in colour type 3 image.")
- plte = group(array('B', self.plte), 3)
- if self.trns or alpha == 'force':
- trns = array('B', self.trns or '')
- trns.extend([255]*(len(plte)-len(trns)))
- plte = list(map(operator.add, plte, group(trns, 1)))
- return plte
-
- def asDirect(self):
- """Returns the image data as a direct representation of an
- ``x * y * planes`` array. This method is intended to remove the
- need for callers to deal with palettes and transparency
- themselves. Images with a palette (colour type 3)
- are converted to RGB or RGBA; images with transparency (a
- ``tRNS`` chunk) are converted to LA or RGBA as appropriate.
- When returned in this format the pixel values represent the
- colour value directly without needing to refer to palettes or
- transparency information.
-
- Like the :meth:`read` method this method returns a 4-tuple:
-
- (*width*, *height*, *pixels*, *meta*)
-
- This method normally returns pixel values with the bit depth
- they have in the source image, but when the source PNG has an
- ``sBIT`` chunk it is inspected and can reduce the bit depth of
- the result pixels; pixel values will be reduced according to
- the bit depth specified in the ``sBIT`` chunk (PNG nerds should
- note a single result bit depth is used for all channels; the
- maximum of the ones specified in the ``sBIT`` chunk. An RGB565
- image will be rescaled to 6-bit RGB666).
-
- The *meta* dictionary that is returned reflects the `direct`
- format and not the original source image. For example, an RGB
- source image with a ``tRNS`` chunk to represent a transparent
- colour, will have ``planes=3`` and ``alpha=False`` for the
- source image, but the *meta* dictionary returned by this method
- will have ``planes=4`` and ``alpha=True`` because an alpha
- channel is synthesized and added.
-
- *pixels* is the pixel data in boxed row flat pixel format (just
- like the :meth:`read` method).
-
- All the other aspects of the image data are not changed.
- """
-
- self.preamble()
-
- # Simple case, no conversion necessary.
- if not self.colormap and not self.trns and not self.sbit:
- return self.read()
-
- x,y,pixels,meta = self.read()
-
- if self.colormap:
- meta['colormap'] = False
- meta['alpha'] = bool(self.trns)
- meta['bitdepth'] = 8
- meta['planes'] = 3 + bool(self.trns)
- plte = self.palette()
- def iterpal(pixels):
- for row in pixels:
- row = list(map(plte.__getitem__, row))
- yield array('B', itertools.chain(*row))
- pixels = iterpal(pixels)
- elif self.trns:
- # It would be nice if there was some reasonable way of doing
- # this without generating a whole load of intermediate tuples.
- # But tuples does seem like the easiest way, with no other way
- # clearly much simpler or much faster. (Actually, the L to LA
- # conversion could perhaps go faster (all those 1-tuples!), but
- # I still wonder whether the code proliferation is worth it)
- it = self.transparent
- maxval = 2**meta['bitdepth']-1
- planes = meta['planes']
- meta['alpha'] = True
- meta['planes'] += 1
- typecode = 'BH'[meta['bitdepth']>8]
- def itertrns(pixels):
- for row in pixels:
- # For each row we group it into pixels, then form a
- # characterisation vector that says whether each pixel
- # is opaque or not. Then we convert True/False to
- # 0/maxval (by multiplication), and add it as the extra
- # channel.
- row = group(row, planes)
- opa = list(map(it.__ne__, row))
- opa = list(map(maxval.__mul__, opa))
- opa = list(zip(opa)) # convert to 1-tuples
- yield array(typecode,
- itertools.chain(*list(map(operator.add, row, opa))))
- pixels = itertrns(pixels)
- targetbitdepth = None
- if self.sbit:
- sbit = struct.unpack('%dB' % len(self.sbit), self.sbit)
- targetbitdepth = max(sbit)
- if targetbitdepth > meta['bitdepth']:
- raise Error('sBIT chunk %r exceeds bitdepth %d' %
- (sbit,self.bitdepth))
- if min(sbit) <= 0:
- raise Error('sBIT chunk %r has a 0-entry' % sbit)
- if targetbitdepth == meta['bitdepth']:
- targetbitdepth = None
- if targetbitdepth:
- shift = meta['bitdepth'] - targetbitdepth
- meta['bitdepth'] = targetbitdepth
- def itershift(pixels):
- for row in pixels:
- yield list(map(shift.__rrshift__, row))
- pixels = itershift(pixels)
- return x,y,pixels,meta
-
- def asFloat(self, maxval=1.0):
- """Return image pixels as per :meth:`asDirect` method, but scale
- all pixel values to be floating point values between 0.0 and
- *maxval*.
- """
-
- x,y,pixels,info = self.asDirect()
- sourcemaxval = 2**info['bitdepth']-1
- del info['bitdepth']
- info['maxval'] = float(maxval)
- factor = float(maxval)/float(sourcemaxval)
- def iterfloat():
- for row in pixels:
- yield list(map(factor.__mul__, row))
- return x,y,iterfloat(),info
-
- def _as_rescale(self, get, targetbitdepth):
- """Helper used by :meth:`asRGB8` and :meth:`asRGBA8`."""
-
- width,height,pixels,meta = get()
- maxval = 2**meta['bitdepth'] - 1
- targetmaxval = 2**targetbitdepth - 1
- factor = float(targetmaxval) / float(maxval)
- meta['bitdepth'] = targetbitdepth
- def iterscale():
- for row in pixels:
- yield [int(round(x*factor)) for x in row]
- if maxval == targetmaxval:
- return width, height, pixels, meta
- else:
- return width, height, iterscale(), meta
-
- def asRGB8(self):
- """Return the image data as an RGB pixels with 8-bits per
- sample. This is like the :meth:`asRGB` method except that
- this method additionally rescales the values so that they
- are all between 0 and 255 (8-bit). In the case where the
- source image has a bit depth < 8 the transformation preserves
- all the information; where the source image has bit depth
- > 8, then rescaling to 8-bit values loses precision. No
- dithering is performed. Like :meth:`asRGB`, an alpha channel
- in the source image will raise an exception.
-
- This function returns a 4-tuple:
- (*width*, *height*, *pixels*, *metadata*).
- *width*, *height*, *metadata* are as per the :meth:`read` method.
-
- *pixels* is the pixel data in boxed row flat pixel format.
- """
-
- return self._as_rescale(self.asRGB, 8)
-
- def asRGBA8(self):
- """Return the image data as RGBA pixels with 8-bits per
- sample. This method is similar to :meth:`asRGB8` and
- :meth:`asRGBA`: The result pixels have an alpha channel, *and*
- values are rescaled to the range 0 to 255. The alpha channel is
- synthesized if necessary (with a small speed penalty).
- """
-
- return self._as_rescale(self.asRGBA, 8)
-
- def asRGB(self):
- """Return image as RGB pixels. RGB colour images are passed
- through unchanged; greyscales are expanded into RGB
- triplets (there is a small speed overhead for doing this).
-
- An alpha channel in the source image will raise an
- exception.
-
- The return values are as for the :meth:`read` method
- except that the *metadata* reflect the returned pixels, not the
- source image. In particular, for this method
- ``metadata['greyscale']`` will be ``False``.
- """
-
- width,height,pixels,meta = self.asDirect()
- if meta['alpha']:
- raise Error("will not convert image with alpha channel to RGB")
- if not meta['greyscale']:
- return width,height,pixels,meta
- meta['greyscale'] = False
- typecode = 'BH'[meta['bitdepth'] > 8]
- def iterrgb():
- for row in pixels:
- a = array(typecode, [0]) * 3 * width
- for i in range(3):
- a[i::3] = row
- yield a
- return width,height,iterrgb(),meta
-
- def asRGBA(self):
- """Return image as RGBA pixels. Greyscales are expanded into
- RGB triplets; an alpha channel is synthesized if necessary.
- The return values are as for the :meth:`read` method
- except that the *metadata* reflect the returned pixels, not the
- source image. In particular, for this method
- ``metadata['greyscale']`` will be ``False``, and
- ``metadata['alpha']`` will be ``True``.
- """
-
- width,height,pixels,meta = self.asDirect()
- if meta['alpha'] and not meta['greyscale']:
- return width,height,pixels,meta
- typecode = 'BH'[meta['bitdepth'] > 8]
- maxval = 2**meta['bitdepth'] - 1
- maxbuffer = struct.pack('=' + typecode, maxval) * 4 * width
- def newarray():
- return array(typecode, maxbuffer)
-
- if meta['alpha'] and meta['greyscale']:
- # LA to RGBA
- def convert():
- for row in pixels:
- # Create a fresh target row, then copy L channel
- # into first three target channels, and A channel
- # into fourth channel.
- a = newarray()
- pngfilters.convert_la_to_rgba(row, a)
- yield a
- elif meta['greyscale']:
- # L to RGBA
- def convert():
- for row in pixels:
- a = newarray()
- pngfilters.convert_l_to_rgba(row, a)
- yield a
- else:
- assert not meta['alpha'] and not meta['greyscale']
- # RGB to RGBA
- def convert():
- for row in pixels:
- a = newarray()
- pngfilters.convert_rgb_to_rgba(row, a)
- yield a
- meta['alpha'] = True
- meta['greyscale'] = False
- return width,height,convert(),meta
-
-
-# === Legacy Version Support ===
-
-# :pyver:old: PyPNG works on Python versions 2.3 and 2.2, but not
-# without some awkward problems. Really PyPNG works on Python 2.4 (and
-# above); it works on Pythons 2.3 and 2.2 by virtue of fixing up
-# problems here. It's a bit ugly (which is why it's hidden down here).
-#
-# Generally the strategy is one of pretending that we're running on
-# Python 2.4 (or above), and patching up the library support on earlier
-# versions so that it looks enough like Python 2.4. When it comes to
-# Python 2.2 there is one thing we cannot patch: extended slices
-# http://www.python.org/doc/2.3/whatsnew/section-slices.html.
-# Instead we simply declare that features that are implemented using
-# extended slices will not work on Python 2.2.
-#
-# In order to work on Python 2.3 we fix up a recurring annoyance involving
-# the array type. In Python 2.3 an array cannot be initialised with an
-# array, and it cannot be extended with a list (or other sequence).
-# Both of those are repeated issues in the code. Whilst I would not
-# normally tolerate this sort of behaviour, here we "shim" a replacement
-# for array into place (and hope no-ones notices). You never read this.
-#
-# In an amusing case of warty hacks on top of warty hacks... the array
-# shimming we try and do only works on Python 2.3 and above (you can't
-# subclass array.array in Python 2.2). So to get it working on Python
-# 2.2 we go for something much simpler and (probably) way slower.
-try:
- array('B').extend([])
- array('B', array('B'))
-except:
- # Expect to get here on Python 2.3
- try:
- class _array_shim(array):
- true_array = array
- def __new__(cls, typecode, init=None):
- super_new = super(_array_shim, cls).__new__
- it = super_new(cls, typecode)
- if init is None:
- return it
- it.extend(init)
- return it
- def extend(self, extension):
- super_extend = super(_array_shim, self).extend
- if isinstance(extension, self.true_array):
- return super_extend(extension)
- if not isinstance(extension, (list, str)):
- # Convert to list. Allows iterators to work.
- extension = list(extension)
- return super_extend(self.true_array(self.typecode, extension))
- array = _array_shim
- except:
- # Expect to get here on Python 2.2
- def array(typecode, init=()):
- if type(init) == str:
- return list(map(ord, init))
- return list(init)
-
-# Further hacks to get it limping along on Python 2.2
-try:
- enumerate
-except:
- def enumerate(seq):
- i=0
- for x in seq:
- yield i,x
- i += 1
-
-try:
- reversed
-except:
- def reversed(l):
- l = list(l)
- l.reverse()
- for x in l:
- yield x
-
-try:
- itertools
-except:
- class _dummy_itertools:
- pass
- itertools = _dummy_itertools()
- def _itertools_imap(f, seq):
- for x in seq:
- yield f(x)
- itertools.imap = _itertools_imap
- def _itertools_chain(*iterables):
- for it in iterables:
- for element in it:
- yield element
- itertools.chain = _itertools_chain
-
-
-# === Support for users without Cython ===
-
-try:
- pngfilters
-except:
- class pngfilters(object):
- def undo_filter_sub(filter_unit, scanline, previous, result):
- """Undo sub filter."""
-
- ai = 0
- # Loops starts at index fu. Observe that the initial part
- # of the result is already filled in correctly with
- # scanline.
- for i in range(filter_unit, len(result)):
- x = scanline[i]
- a = result[ai]
- result[i] = (x + a) & 0xff
- ai += 1
- undo_filter_sub = staticmethod(undo_filter_sub)
-
- def undo_filter_up(filter_unit, scanline, previous, result):
- """Undo up filter."""
-
- for i in range(len(result)):
- x = scanline[i]
- b = previous[i]
- result[i] = (x + b) & 0xff
- undo_filter_up = staticmethod(undo_filter_up)
-
- def undo_filter_average(filter_unit, scanline, previous, result):
- """Undo up filter."""
-
- ai = -filter_unit
- for i in range(len(result)):
- x = scanline[i]
- if ai < 0:
- a = 0
- else:
- a = result[ai]
- b = previous[i]
- result[i] = (x + ((a + b) >> 1)) & 0xff
- ai += 1
- undo_filter_average = staticmethod(undo_filter_average)
-
- def undo_filter_paeth(filter_unit, scanline, previous, result):
- """Undo Paeth filter."""
-
- # Also used for ci.
- ai = -filter_unit
- for i in range(len(result)):
- x = scanline[i]
- if ai < 0:
- a = c = 0
- else:
- a = result[ai]
- c = previous[ai]
- b = previous[i]
- p = a + b - c
- pa = abs(p - a)
- pb = abs(p - b)
- pc = abs(p - c)
- if pa <= pb and pa <= pc:
- pr = a
- elif pb <= pc:
- pr = b
- else:
- pr = c
- result[i] = (x + pr) & 0xff
- ai += 1
- undo_filter_paeth = staticmethod(undo_filter_paeth)
-
- def convert_la_to_rgba(row, result):
- for i in range(3):
- result[i::4] = row[0::2]
- result[3::4] = row[1::2]
- convert_la_to_rgba = staticmethod(convert_la_to_rgba)
-
- def convert_l_to_rgba(row, result):
- """Convert a grayscale image to RGBA. This method assumes the alpha
- channel in result is already correctly initialized."""
- for i in range(3):
- result[i::4] = row
- convert_l_to_rgba = staticmethod(convert_l_to_rgba)
-
- def convert_rgb_to_rgba(row, result):
- """Convert an RGB image to RGBA. This method assumes the alpha
- channel in result is already correctly initialized."""
- for i in range(3):
- result[i::4] = row[i::3]
- convert_rgb_to_rgba = staticmethod(convert_rgb_to_rgba)
-
-
-# === Internal Test Support ===
-
-# This section comprises the tests that are internally validated (as
-# opposed to tests which produce output files that are externally
-# validated). Primarily they are unittests.
-
-# Note that it is difficult to internally validate the results of
-# writing a PNG file. The only thing we can do is read it back in
-# again, which merely checks consistency, not that the PNG file we
-# produce is valid.
-
-# Run the tests from the command line:
-# python -c 'import png;png.runTest()'
-# If you have nose installed you can use that:
-# nosetests png.py
-
-# (For an in-memory binary file IO object) We use BytesIO where
-# available, otherwise we use StringIO, but name it BytesIO.
-try:
- from io import BytesIO
-except:
- from io import StringIO as BytesIO
-import tempfile
-# http://www.python.org/doc/2.4.4/lib/module-unittest.html
-import unittest
-
-
-def runTest():
- unittest.main(__name__)
-
-def topngbytes(name, rows, x, y, **k):
- """Convenience function for creating a PNG file "in memory" as a
- string. Creates a :class:`Writer` instance using the keyword arguments,
- then passes `rows` to its :meth:`Writer.write` method. The resulting
- PNG file is returned as a string. `name` is used to identify the file for
- debugging.
- """
-
- import os
-
- print(name)
- f = BytesIO()
- w = Writer(x, y, **k)
- w.write(f, rows)
- if os.environ.get('PYPNG_TEST_TMP'):
- w = open(name, 'wb')
- w.write(f.getvalue())
- w.close()
- return f.getvalue()
-
-def _redirect_io(inp, out, f):
- """Calls the function `f` with ``sys.stdin`` changed to `inp`
- and ``sys.stdout`` changed to `out`. They are restored when `f`
- returns. This function returns whatever `f` returns.
- """
-
- import os
-
- try:
- oldin,sys.stdin = sys.stdin,inp
- oldout,sys.stdout = sys.stdout,out
- x = f()
- finally:
- sys.stdin = oldin
- sys.stdout = oldout
- if os.environ.get('PYPNG_TEST_TMP') and hasattr(out,'getvalue'):
- name = mycallersname()
- if name:
- w = open(name+'.png', 'wb')
- w.write(out.getvalue())
- w.close()
- return x
-
-def mycallersname():
- """Returns the name of the caller of the caller of this function
- (hence the name of the caller of the function in which
- "mycallersname()" textually appears). Returns None if this cannot
- be determined."""
-
- # http://docs.python.org/library/inspect.html#the-interpreter-stack
- import inspect
-
- frame = inspect.currentframe()
- if not frame:
- return None
- frame_,filename_,lineno_,funname,linelist_,listi_ = (
- inspect.getouterframes(frame)[2])
- return funname
-
-def seqtobytes(s):
- """Convert a sequence of integers to a *bytes* instance. Good for
- plastering over Python 2 / Python 3 cracks.
- """
-
- return strtobytes(''.join(chr(x) for x in s))
-
-class Test(unittest.TestCase):
- # This member is used by the superclass. If we don't define a new
- # class here then when we use self.assertRaises() and the PyPNG code
- # raises an assertion then we get no proper traceback. I can't work
- # out why, but defining a new class here means we get a proper
- # traceback.
- class failureException(Exception):
- pass
-
- def helperLN(self, n):
- mask = (1 << n) - 1
- # Use small chunk_limit so that multiple chunk writing is
- # tested. Making it a test for Issue 20.
- w = Writer(15, 17, greyscale=True, bitdepth=n, chunk_limit=99)
- f = BytesIO()
- w.write_array(f, array('B', list(map(mask.__and__, list(range(1, 256))))))
- r = Reader(bytes=f.getvalue())
- x,y,pixels,meta = r.read()
- self.assertEqual(x, 15)
- self.assertEqual(y, 17)
- self.assertEqual(list(itertools.chain(*pixels)),
- list(map(mask.__and__, list(range(1,256)))))
- def testL8(self):
- return self.helperLN(8)
- def testL4(self):
- return self.helperLN(4)
- def testL2(self):
- "Also tests asRGB8."
- w = Writer(1, 4, greyscale=True, bitdepth=2)
- f = BytesIO()
- w.write_array(f, array('B', list(range(4))))
- r = Reader(bytes=f.getvalue())
- x,y,pixels,meta = r.asRGB8()
- self.assertEqual(x, 1)
- self.assertEqual(y, 4)
- for i,row in enumerate(pixels):
- self.assertEqual(len(row), 3)
- self.assertEqual(list(row), [0x55*i]*3)
- def testP2(self):
- "2-bit palette."
- a = (255,255,255)
- b = (200,120,120)
- c = (50,99,50)
- w = Writer(1, 4, bitdepth=2, palette=[a,b,c])
- f = BytesIO()
- w.write_array(f, array('B', (0,1,1,2)))
- r = Reader(bytes=f.getvalue())
- x,y,pixels,meta = r.asRGB8()
- self.assertEqual(x, 1)
- self.assertEqual(y, 4)
- self.assertEqual(list(map(list, pixels)), list(map(list, [a, b, b, c])))
- def testPtrns(self):
- "Test colour type 3 and tRNS chunk (and 4-bit palette)."
- a = (50,99,50,50)
- b = (200,120,120,80)
- c = (255,255,255)
- d = (200,120,120)
- e = (50,99,50)
- w = Writer(3, 3, bitdepth=4, palette=[a,b,c,d,e])
- f = BytesIO()
- w.write_array(f, array('B', (4, 3, 2, 3, 2, 0, 2, 0, 1)))
- r = Reader(bytes=f.getvalue())
- x,y,pixels,meta = r.asRGBA8()
- self.assertEqual(x, 3)
- self.assertEqual(y, 3)
- c = c+(255,)
- d = d+(255,)
- e = e+(255,)
- boxed = [(e,d,c),(d,c,a),(c,a,b)]
- flat = [itertools.chain(*row) for row in boxed]
- self.assertEqual(list(map(list, pixels)), list(map(list, flat)))
- def testRGBtoRGBA(self):
- "asRGBA8() on colour type 2 source."""
- # Test for Issue 26
- # Also test that Reader can take a "file-like" object.
- r = Reader(BytesIO(_pngsuite['basn2c08']))
- x,y,pixels,meta = r.asRGBA8()
- # Test the pixels at row 9 columns 0 and 1.
- row9 = list(pixels)[9]
- self.assertEqual(list(row9[0:8]),
- [0xff, 0xdf, 0xff, 0xff, 0xff, 0xde, 0xff, 0xff])
- def testLtoRGBA(self):
- "asRGBA() on grey source."""
- # Test for Issue 60
- r = Reader(bytes=_pngsuite['basi0g08'])
- x,y,pixels,meta = r.asRGBA()
- row9 = list(list(pixels)[9])
- self.assertEqual(row9[0:8],
- [222, 222, 222, 255, 221, 221, 221, 255])
- def testCtrns(self):
- "Test colour type 2 and tRNS chunk."
- # Test for Issue 25
- r = Reader(bytes=_pngsuite['tbrn2c08'])
- x,y,pixels,meta = r.asRGBA8()
- # I just happen to know that the first pixel is transparent.
- # In particular it should be #7f7f7f00
- row0 = list(pixels)[0]
- self.assertEqual(tuple(row0[0:4]), (0x7f, 0x7f, 0x7f, 0x00))
- def testAdam7read(self):
- """Adam7 interlace reading.
- Specifically, test that for images in the PngSuite that
- have both an interlaced and straightlaced pair that both
- images from the pair produce the same array of pixels."""
- for candidate in _pngsuite:
- if not candidate.startswith('basn'):
- continue
- candi = candidate.replace('n', 'i')
- if candi not in _pngsuite:
- continue
- print('adam7 read', candidate)
- straight = Reader(bytes=_pngsuite[candidate])
- adam7 = Reader(bytes=_pngsuite[candi])
- # Just compare the pixels. Ignore x,y (because they're
- # likely to be correct?); metadata is ignored because the
- # "interlace" member differs. Lame.
- straight = straight.read()[2]
- adam7 = adam7.read()[2]
- self.assertEqual(list(map(list, straight)), list(map(list, adam7)))
- def testAdam7write(self):
- """Adam7 interlace writing.
- For each test image in the PngSuite, write an interlaced
- and a straightlaced version. Decode both, and compare results.
- """
- # Not such a great test, because the only way we can check what
- # we have written is to read it back again.
-
- for name,bytes in list(_pngsuite.items()):
- # Only certain colour types supported for this test.
- if name[3:5] not in ['n0', 'n2', 'n4', 'n6']:
- continue
- it = Reader(bytes=bytes)
- x,y,pixels,meta = it.read()
- pngi = topngbytes('adam7wn'+name+'.png', pixels,
- x=x, y=y, bitdepth=it.bitdepth,
- greyscale=it.greyscale, alpha=it.alpha,
- transparent=it.transparent,
- interlace=False)
- x,y,ps,meta = Reader(bytes=pngi).read()
- it = Reader(bytes=bytes)
- x,y,pixels,meta = it.read()
- pngs = topngbytes('adam7wi'+name+'.png', pixels,
- x=x, y=y, bitdepth=it.bitdepth,
- greyscale=it.greyscale, alpha=it.alpha,
- transparent=it.transparent,
- interlace=True)
- x,y,pi,meta = Reader(bytes=pngs).read()
- self.assertEqual(list(map(list, ps)), list(map(list, pi)))
- def testPGMin(self):
- """Test that the command line tool can read PGM files."""
- def do():
- return _main(['testPGMin'])
- s = BytesIO()
- s.write(strtobytes('P5 2 2 3\n'))
- s.write(strtobytes('\x00\x01\x02\x03'))
- s.flush()
- s.seek(0)
- o = BytesIO()
- _redirect_io(s, o, do)
- r = Reader(bytes=o.getvalue())
- x,y,pixels,meta = r.read()
- self.assertTrue(r.greyscale)
- self.assertEqual(r.bitdepth, 2)
- def testPAMin(self):
- """Test that the command line tool can read PAM file."""
- def do():
- return _main(['testPAMin'])
- s = BytesIO()
- s.write(strtobytes('P7\nWIDTH 3\nHEIGHT 1\nDEPTH 4\nMAXVAL 255\n'
- 'TUPLTYPE RGB_ALPHA\nENDHDR\n'))
- # The pixels in flat row flat pixel format
- flat = [255,0,0,255, 0,255,0,120, 0,0,255,30]
- asbytes = seqtobytes(flat)
- s.write(asbytes)
- s.flush()
- s.seek(0)
- o = BytesIO()
- _redirect_io(s, o, do)
- r = Reader(bytes=o.getvalue())
- x,y,pixels,meta = r.read()
- self.assertTrue(r.alpha)
- self.assertTrue(not r.greyscale)
- self.assertEqual(list(itertools.chain(*pixels)), flat)
- def testLA4(self):
- """Create an LA image with bitdepth 4."""
- bytes = topngbytes('la4.png', [[5, 12]], 1, 1,
- greyscale=True, alpha=True, bitdepth=4)
- sbit = Reader(bytes=bytes).chunk('sBIT')[1]
- self.assertEqual(sbit, strtobytes('\x04\x04'))
- def testPal(self):
- """Test that a palette PNG returns the palette in info."""
- r = Reader(bytes=_pngsuite['basn3p04'])
- x,y,pixels,info = r.read()
- self.assertEqual(x, 32)
- self.assertEqual(y, 32)
- self.assertTrue('palette' in info)
- def testPalWrite(self):
- """Test metadata for paletted PNG can be passed from one PNG
- to another."""
- r = Reader(bytes=_pngsuite['basn3p04'])
- x,y,pixels,info = r.read()
- w = Writer(**info)
- o = BytesIO()
- w.write(o, pixels)
- o.flush()
- o.seek(0)
- r = Reader(file=o)
- _,_,_,again_info = r.read()
- # Same palette
- self.assertEqual(again_info['palette'], info['palette'])
- def testPalExpand(self):
- """Test that bitdepth can be used to fiddle with pallete image."""
- r = Reader(bytes=_pngsuite['basn3p04'])
- x,y,pixels,info = r.read()
- pixels = [list(row) for row in pixels]
- info['bitdepth'] = 8
- w = Writer(**info)
- o = BytesIO()
- w.write(o, pixels)
- o.flush()
- o.seek(0)
- r = Reader(file=o)
- _,_,again_pixels,again_info = r.read()
- # Same pixels
- again_pixels = [list(row) for row in again_pixels]
- self.assertEqual(again_pixels, pixels)
-
- def testPNMsbit(self):
- """Test that PNM files can generates sBIT chunk."""
- def do():
- return _main(['testPNMsbit'])
- s = BytesIO()
- s.write(strtobytes('P6 8 1 1\n'))
- for pixel in range(8):
- s.write(struct.pack(' 0xff:
- fmt = fmt + 'H'
- else:
- fmt = fmt + 'B'
- for row in pixels:
- file.write(struct.pack(fmt, *row))
- file.flush()
-
-def color_triple(color):
- """
- Convert a command line colour value to a RGB triple of integers.
- FIXME: Somewhere we need support for greyscale backgrounds etc.
- """
- if color.startswith('#') and len(color) == 4:
- return (int(color[1], 16),
- int(color[2], 16),
- int(color[3], 16))
- if color.startswith('#') and len(color) == 7:
- return (int(color[1:3], 16),
- int(color[3:5], 16),
- int(color[5:7], 16))
- elif color.startswith('#') and len(color) == 13:
- return (int(color[1:5], 16),
- int(color[5:9], 16),
- int(color[9:13], 16))
-
-def _add_common_options(parser):
- """Call *parser.add_option* for each of the options that are
- common between this PNG--PNM conversion tool and the gen
- tool.
- """
- parser.add_option("-i", "--interlace",
- default=False, action="store_true",
- help="create an interlaced PNG file (Adam7)")
- parser.add_option("-t", "--transparent",
- action="store", type="string", metavar="#RRGGBB",
- help="mark the specified colour as transparent")
- parser.add_option("-b", "--background",
- action="store", type="string", metavar="#RRGGBB",
- help="save the specified background colour")
- parser.add_option("-g", "--gamma",
- action="store", type="float", metavar="value",
- help="save the specified gamma value")
- parser.add_option("-c", "--compression",
- action="store", type="int", metavar="level",
- help="zlib compression level (0-9)")
- return parser
-
-def _main(argv):
- """
- Run the PNG encoder with options from the command line.
- """
-
- # Parse command line arguments
- from optparse import OptionParser
- import re
- version = '%prog ' + re.sub(r'( ?\$|URL: |Rev:)', '', __version__)
- parser = OptionParser(version=version)
- parser.set_usage("%prog [options] [imagefile]")
- parser.add_option('-r', '--read-png', default=False,
- action='store_true',
- help='Read PNG, write PNM')
- parser.add_option("-a", "--alpha",
- action="store", type="string", metavar="pgmfile",
- help="alpha channel transparency (RGBA)")
- _add_common_options(parser)
-
- (options, args) = parser.parse_args(args=argv[1:])
-
- # Convert options
- if options.transparent is not None:
- options.transparent = color_triple(options.transparent)
- if options.background is not None:
- options.background = color_triple(options.background)
-
- # Prepare input and output files
- if len(args) == 0:
- infilename = '-'
- infile = sys.stdin
- elif len(args) == 1:
- infilename = args[0]
- infile = open(infilename, 'rb')
- else:
- parser.error("more than one input file")
- outfile = sys.stdout
- if sys.platform == "win32":
- import msvcrt, os
- msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
-
- if options.read_png:
- # Encode PNG to PPM
- png = Reader(file=infile)
- width,height,pixels,meta = png.asDirect()
- write_pnm(outfile, width, height, pixels, meta)
- else:
- # Encode PNM to PNG
- format, width, height, depth, maxval = \
- read_pnm_header(infile, ('P5','P6','P7'))
- # When it comes to the variety of input formats, we do something
- # rather rude. Observe that L, LA, RGB, RGBA are the 4 colour
- # types supported by PNG and that they correspond to 1, 2, 3, 4
- # channels respectively. So we use the number of channels in
- # the source image to determine which one we have. We do not
- # care about TUPLTYPE.
- greyscale = depth <= 2
- pamalpha = depth in (2,4)
- supported = [2**x-1 for x in range(1,17)]
- try:
- mi = supported.index(maxval)
- except ValueError:
- raise NotImplementedError(
- 'your maxval (%s) not in supported list %s' %
- (maxval, str(supported)))
- bitdepth = mi+1
- writer = Writer(width, height,
- greyscale=greyscale,
- bitdepth=bitdepth,
- interlace=options.interlace,
- transparent=options.transparent,
- background=options.background,
- alpha=bool(pamalpha or options.alpha),
- gamma=options.gamma,
- compression=options.compression)
- if options.alpha:
- pgmfile = open(options.alpha, 'rb')
- format, awidth, aheight, adepth, amaxval = \
- read_pnm_header(pgmfile, 'P5')
- if amaxval != '255':
- raise NotImplementedError(
- 'maxval %s not supported for alpha channel' % amaxval)
- if (awidth, aheight) != (width, height):
- raise ValueError("alpha channel image size mismatch"
- " (%s has %sx%s but %s has %sx%s)"
- % (infilename, width, height,
- options.alpha, awidth, aheight))
- writer.convert_ppm_and_pgm(infile, pgmfile, outfile)
- else:
- writer.convert_pnm(infile, outfile)
-
-
-if __name__ == '__main__':
- try:
- _main(sys.argv)
- except Error as e:
- print(e, file=sys.stderr)
diff --git a/pyqrcode/tables.py b/pyqrcode/tables.py
deleted file mode 100644
index 518688c..0000000
--- a/pyqrcode/tables.py
+++ /dev/null
@@ -1,746 +0,0 @@
-'''This module lists out all of the tables needed to create a QR code.
-If you are viewing this in the HTML documentation, I recommend reading the
-actual file instead. The formating for the tables is much more readable.
-'''
-
-#: This defines the QR Code's 'mode' which sets what
-#: type of code it is along with its size.
-modes = {'numeric':1,
- '1':1,
- 1:1,
- 'alphanumeric':2,
- 'alfanumeric':2,
- 'text':2,
- '2':2,
- 2:2,
- 'binary':4,
- 'bin':4,
- 'byte':4,
- 'bytes':4,
- '4':4,
- 4:4,
- 'japanese':8,
- 'kanji':8,
- '8':8,
- 8:8}
-
-#: This defines the amount of error correction. The dictionary
-#: allows the user to specify this in several ways.
-error_level = {'L':'L', 'l':'L', '7%':'L', .7:'L',
- 'M':'M', 'm':'M', '15%':'M', .15:'M',
- 'Q':'Q', 'q':'Q', '25%':'Q', .25:'Q',
- 'H':'H', 'h':'H', '30%':'H', .30:'H'}
-
-#: This is a dictionary holds how long the "data length" field is for
-#: each version and mode of the QR Code.
-data_length_field = {9:{1:10, 2:9, 4:8, 8:8},
- 26:{1:12, 2:11, 4:16, 8:10},
- 40:{1:14, 2:13, 4:16, 8:12}}
-
-#: QR Codes uses a unique ASCII-like table for the 'alphanumeric' mode.
-#: This is a dictionary representing that unique table, where the
-#: keys are the possible characters in the data and the values
-#: are the character's numeric representation.
-ascii_codes = {'0':0, '1':1, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7,
- '8':8, '9':9, 'A':10, 'B':11, 'C':12, 'D':13, 'E':14,
- 'F':15, 'G':16, 'H':17, 'I':18, 'J':19, 'K':20, 'L':21,
- 'M':22, 'N':23, 'O':24, 'P':25, 'Q':26, 'R':27, 'S':28,
- 'T':29, 'U':30, 'V':31, 'W':32, 'X':33, 'Y':34, 'Z':35,
- ' ':36, '$':37, '%':38, '*':39, '+':40, '-':41, '.':42,
- '/':43, ':':44}
-
-#: This array specifies the size of a QR Code in pixels. These numbers are
-#: defined in the standard. The indexes correspond to the QR Code's
-#: version number. This array was taken from:
-#:
-#: http://www.denso-wave.com/qrcode/vertable1-e.html
-version_size = [None, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57,
- 61, 65, 69, 73, 77, 81, 85, 89, 93, 97,
- 101, 105, 109, 113, 117, 121, 125, 129, 133, 137,
- 141, 145, 149, 153, 157, 161, 165, 169, 173, 177]
-
-#: This dictionary lists the data capacity for all possible QR Codes.
-#: This dictionary is organized where the first key corresponds to the
-#: QR Code version number. The next key corresponds to the error
-#: correction level, see error. The final key corresponds to
-#: the mode number, see modes. The zero mode number represents the
-#: possible "data bits." This table was taken from:
-#:
-#: http://www.denso-wave.com/qrcode/vertable1-e.html
-data_capacity = {
- 1: {
- "L":{0:152,1:41,2:25,4:17,8:10,},
- "M":{0:128,1:34,2:20,4:14,8:8,},
- "Q":{0:104,1:27,2:16,4:11,8:7,},
- "H":{0:72,1:17,2:10,4:7,8:4,}},
- 2: {
- "L":{0:272,1:77,2:47,4:32,8:20,},
- "M":{0:224,1:63,2:38,4:26,8:16,},
- "Q":{0:176,1:48,2:29,4:20,8:12,},
- "H":{0:128,1:34,2:20,4:14,8:8,}},
- 3: {
- "L":{0:440,1:127,2:77,4:53,8:32,},
- "M":{0:352,1:101,2:61,4:42,8:26,},
- "Q":{0:272,1:77,2:47,4:32,8:20,},
- "H":{0:208,1:58,2:35,4:24,8:15,}},
- 4: {
- "L":{0:640,1:187,2:114,4:78,8:48,},
- "M":{0:512,1:149,2:90,4:62,8:38,},
- "Q":{0:384,1:111,2:67,4:46,8:28,},
- "H":{0:288,1:82,2:50,4:34,8:21,}},
- 5: {
- "L":{0:864,1:255,2:154,4:106,8:65,},
- "M":{0:688,1:202,2:122,4:84,8:52,},
- "Q":{0:496,1:144,2:87,4:60,8:37,},
- "H":{0:368,1:106,2:64,4:44,8:27,}},
- 6: {
- "L":{0:1088,1:322,2:195,4:134,8:82,},
- "M":{0:864,1:255,2:154,4:106,8:65,},
- "Q":{0:608,1:178,2:108,4:74,8:45,},
- "H":{0:480,1:139,2:84,4:58,8:36,}},
- 7: {
- "L":{0:1248,1:370,2:224,4:154,8:95,},
- "M":{0:992,1:293,2:178,4:122,8:75,},
- "Q":{0:704,1:207,2:125,4:86,8:53,},
- "H":{0:528,1:154,2:93,4:64,8:39,}},
- 8: {
- "L":{0:1552,1:461,2:279,4:192,8:118,},
- "M":{0:1232,1:365,2:221,4:152,8:93,},
- "Q":{0:880,1:259,2:157,4:108,8:66,},
- "H":{0:688,1:202,2:122,4:84,8:52,}},
- 9: {
- "L":{0:1856,1:552,2:335,4:230,8:141,},
- "M":{0:1456,1:432,2:262,4:180,8:111,},
- "Q":{0:1056,1:312,2:189,4:130,8:80,},
- "H":{0:800,1:235,2:143,4:98,8:60,}},
- 10: {
- "L":{0:2192,1:652,2:395,4:271,8:167,},
- "M":{0:1728,1:513,2:311,4:213,8:131,},
- "Q":{0:1232,1:364,2:221,4:151,8:93,},
- "H":{0:976,1:288,2:174,4:119,8:74,}},
- 11: {
- "L":{0:2592,1:772,2:468,4:321,8:198,},
- "M":{0:2032,1:604,2:366,4:251,8:155,},
- "Q":{0:1440,1:427,2:259,4:177,8:109,},
- "H":{0:1120,1:331,2:200,4:137,8:85,}},
- 12: {
- "L":{0:2960,1:883,2:535,4:367,8:226,},
- "M":{0:2320,1:691,2:419,4:287,8:177,},
- "Q":{0:1648,1:489,2:296,4:203,8:125,},
- "H":{0:1264,1:374,2:227,4:155,8:96,}},
- 13: {
- "L":{0:3424,1:1022,2:619,4:425,8:262,},
- "M":{0:2672,1:796,2:483,4:331,8:204,},
- "Q":{0:1952,1:580,2:352,4:241,8:149,},
- "H":{0:1440,1:427,2:259,4:177,8:109,}},
- 14: {
- "L":{0:3688,1:1101,2:667,4:458,8:282,},
- "M":{0:2920,1:871,2:528,4:362,8:223,},
- "Q":{0:2088,1:621,2:376,4:258,8:159,},
- "H":{0:1576,1:468,2:283,4:194,8:120,}},
- 15: {
- "L":{0:4184,1:1250,2:758,4:520,8:320,},
- "M":{0:3320,1:991,2:600,4:412,8:254,},
- "Q":{0:2360,1:703,2:426,4:292,8:180,},
- "H":{0:1784,1:530,2:321,4:220,8:136,}},
- 16: {
- "L":{0:4712,1:1408,2:854,4:586,8:361,},
- "M":{0:3624,1:1082,2:656,4:450,8:277,},
- "Q":{0:2600,1:775,2:470,4:322,8:198,},
- "H":{0:2024,1:602,2:365,4:250,8:154,}},
- 17: {
- "L":{0:5176,1:1548,2:938,4:644,8:397,},
- "M":{0:4056,1:1212,2:734,4:504,8:310,},
- "Q":{0:2936,1:876,2:531,4:364,8:224,},
- "H":{0:2264,1:674,2:408,4:280,8:173,}},
- 18: {
- "L":{0:5768,1:1725,2:1046,4:718,8:442,},
- "M":{0:4504,1:1346,2:816,4:560,8:345,},
- "Q":{0:3176,1:948,2:574,4:394,8:243,},
- "H":{0:2504,1:746,2:452,4:310,8:191,}},
- 19: {
- "L":{0:6360,1:1903,2:1153,4:792,8:488,},
- "M":{0:5016,1:1500,2:909,4:624,8:384,},
- "Q":{0:3560,1:1063,2:644,4:442,8:272,},
- "H":{0:2728,1:813,2:493,4:338,8:208,}},
- 20: {
- "L":{0:6888,1:2061,2:1249,4:858,8:528,},
- "M":{0:5352,1:1600,2:970,4:666,8:410,},
- "Q":{0:3880,1:1159,2:702,4:482,8:297,},
- "H":{0:3080,1:919,2:557,4:382,8:235,}},
- 21: {
- "L":{0:7456,1:2232,2:1352,4:929,8:572,},
- "M":{0:5712,1:1708,2:1035,4:711,8:438,},
- "Q":{0:4096,1:1224,2:742,4:509,8:314,},
- "H":{0:3248,1:969,2:587,4:403,8:248,}},
- 22: {
- "L":{0:8048,1:2409,2:1460,4:1003,8:618,},
- "M":{0:6256,1:1872,2:1134,4:779,8:480,},
- "Q":{0:4544,1:1358,2:823,4:565,8:348,},
- "H":{0:3536,1:1056,2:640,4:439,8:270,}},
- 23: {
- "L":{0:8752,1:2620,2:1588,4:1091,8:672,},
- "M":{0:6880,1:2059,2:1248,4:857,8:528,},
- "Q":{0:4912,1:1468,2:890,4:611,8:376,},
- "H":{0:3712,1:1108,2:672,4:461,8:284,}},
- 24: {
- "L":{0:9392,1:2812,2:1704,4:1171,8:721,},
- "M":{0:7312,1:2188,2:1326,4:911,8:561,},
- "Q":{0:5312,1:1588,2:963,4:661,8:407,},
- "H":{0:4112,1:1228,2:744,4:511,8:315,}},
- 25: {
- "L":{0:10208,1:3057,2:1853,4:1273,8:784,},
- "M":{0:8000,1:2395,2:1451,4:997,8:614,},
- "Q":{0:5744,1:1718,2:1041,4:715,8:440,},
- "H":{0:4304,1:1286,2:779,4:535,8:330,}},
- 26: {
- "L":{0:10960,1:3283,2:1990,4:1367,8:842,},
- "M":{0:8496,1:2544,2:1542,4:1059,8:652,},
- "Q":{0:6032,1:1804,2:1094,4:751,8:462,},
- "H":{0:4768,1:1425,2:864,4:593,8:365,}},
- 27: {
- "L":{0:11744,1:3514,2:2132,4:1465,8:902,},
- "M":{0:9024,1:2701,2:1637,4:1125,8:692,},
- "Q":{0:6464,1:1933,2:1172,4:805,8:496,},
- "H":{0:5024,1:1501,2:910,4:625,8:385,}},
- 28: {
- "L":{0:12248,1:3669,2:2223,4:1528,8:940,},
- "M":{0:9544,1:2857,2:1732,4:1190,8:732,},
- "Q":{0:6968,1:2085,2:1263,4:868,8:534,},
- "H":{0:5288,1:1581,2:958,4:658,8:405,}},
- 29: {
- "L":{0:13048,1:3909,2:2369,4:1628,8:1002,},
- "M":{0:10136,1:3035,2:1839,4:1264,8:778,},
- "Q":{0:7288,1:2181,2:1322,4:908,8:559,},
- "H":{0:5608,1:1677,2:1016,4:698,8:430,}},
- 30: {
- "L":{0:13880,1:4158,2:2520,4:1732,8:1066,},
- "M":{0:10984,1:3289,2:1994,4:1370,8:843,},
- "Q":{0:7880,1:2358,2:1429,4:982,8:604,},
- "H":{0:5960,1:1782,2:1080,4:742,8:457,}},
- 31: {
- "L":{0:14744,1:4417,2:2677,4:1840,8:1132,},
- "M":{0:11640,1:3486,2:2113,4:1452,8:894,},
- "Q":{0:8264,1:2473,2:1499,4:1030,8:634,},
- "H":{0:6344,1:1897,2:1150,4:790,8:486,}},
- 32: {
- "L":{0:15640,1:4686,2:2840,4:1952,8:1201,},
- "M":{0:12328,1:3693,2:2238,4:1538,8:947,},
- "Q":{0:8920,1:2670,2:1618,4:1112,8:684,},
- "H":{0:6760,1:2022,2:1226,4:842,8:518,}},
- 33: {
- "L":{0:16568,1:4965,2:3009,4:2068,8:1273,},
- "M":{0:13048,1:3909,2:2369,4:1628,8:1002,},
- "Q":{0:9368,1:2805,2:1700,4:1168,8:719,},
- "H":{0:7208,1:2157,2:1307,4:898,8:553,}},
- 34: {
- "L":{0:17528,1:5253,2:3183,4:2188,8:1347,},
- "M":{0:13800,1:4134,2:2506,4:1722,8:1060,},
- "Q":{0:9848,1:2949,2:1787,4:1228,8:756,},
- "H":{0:7688,1:2301,2:1394,4:958,8:590,}},
- 35: {
- "L":{0:18448,1:5529,2:3351,4:2303,8:1417,},
- "M":{0:14496,1:4343,2:2632,4:1809,8:1113,},
- "Q":{0:10288,1:3081,2:1867,4:1283,8:790,},
- "H":{0:7888,1:2361,2:1431,4:983,8:605,}},
- 36: {
- "L":{0:19472,1:5836,2:3537,4:2431,8:1496,},
- "M":{0:15312,1:4588,2:2780,4:1911,8:1176,},
- "Q":{0:10832,1:3244,2:1966,4:1351,8:832,},
- "H":{0:8432,1:2524,2:1530,4:1051,8:647,}},
- 37: {
- "L":{0:20528,1:6153,2:3729,4:2563,8:1577,},
- "M":{0:15936,1:4775,2:2894,4:1989,8:1224,},
- "Q":{0:11408,1:3417,2:2071,4:1423,8:876,},
- "H":{0:8768,1:2625,2:1591,4:1093,8:673,}},
- 38: {
- "L":{0:21616,1:6479,2:3927,4:2699,8:1661,},
- "M":{0:16816,1:5039,2:3054,4:2099,8:1292,},
- "Q":{0:12016,1:3599,2:2181,4:1499,8:923,},
- "H":{0:9136,1:2735,2:1658,4:1139,8:701,}},
- 39: {
- "L":{0:22496,1:6743,2:4087,4:2809,8:1729,},
- "M":{0:17728,1:5313,2:3220,4:2213,8:1362,},
- "Q":{0:12656,1:3791,2:2298,4:1579,8:972,},
- "H":{0:9776,1:2927,2:1774,4:1219,8:750,}},
- 40: {
- "L":{0:23648,1:7089,2:4296,4:2953,8:1817,},
- "M":{0:18672,1:5596,2:3391,4:2331,8:1435,},
- "Q":{0:13328,1:3993,2:2420,4:1663,8:1024,},
- "H":{0:10208,1:3057,2:1852,4:1273,8:784,}}
-}
-
-#: This table defines the "Error Correction Code Words and Block Information."
-#: The table lists the number of error correction words that are required
-#: to be generated for each version and error correction level. The table
-#: is accessed by first using the version number as a key and then the
-#: error level. The array values correspond to these columns from the source
-#: table:
-#:
-#: +----------------------------+
-#: |0 | EC Code Words Per Block |
-#: +----------------------------+
-#: |1 | Block 1 Count |
-#: +----------------------------+
-#: |2 | Block 1 Data Code Words |
-#: +----------------------------+
-#: |3 | Block 2 Count |
-#: +----------------------------+
-#: |4 | Block 2 Data Code Words |
-#: +----------------------------+
-#:
-#: This table was taken from:
-#:
-#: http://www.thonky.com/qr-code-tutorial/error-correction-table/
-eccwbi = {
- 1: {
- 'L': [ 7, 1, 19, 0, 0, ],
- 'M': [10, 1, 16, 0, 0, ],
- 'Q': [13, 1, 13, 0, 0, ],
- 'H': [17, 1, 9, 0, 0, ],
- },
- 2: {
- 'L': [10, 1, 34, 0, 0, ],
- 'M': [16, 1, 28, 0, 0, ],
- 'Q': [22, 1, 22, 0, 0, ],
- 'H': [28, 1, 16, 0, 0, ],
- },
- 3: {
- 'L': [15, 1, 55, 0, 0, ],
- 'M': [26, 1, 44, 0, 0, ],
- 'Q': [18, 2, 17, 0, 0, ],
- 'H': [22, 2, 13, 0, 0, ],
- },
- 4: {
- 'L': [20, 1, 80, 0, 0, ],
- 'M': [18, 2, 32, 0, 0, ],
- 'Q': [26, 2, 24, 0, 0, ],
- 'H': [16, 4, 9, 0, 0, ],
- },
- 5: {
- 'L': [26, 1, 108, 0, 0, ],
- 'M': [24, 2, 43, 0, 0, ],
- 'Q': [18, 2, 15, 2, 16, ],
- 'H': [22, 2, 11, 2, 12, ],
- },
- 6: {
- 'L': [18, 2, 68, 0, 0, ],
- 'M': [16, 4, 27, 0, 0, ],
- 'Q': [24, 4, 19, 0, 0, ],
- 'H': [28, 4, 15, 0, 0, ],
- },
- 7: {
- 'L': [20, 2, 78, 0, 0, ],
- 'M': [18, 4, 31, 0, 0, ],
- 'Q': [18, 2, 14, 4, 15, ],
- 'H': [26, 4, 13, 1, 14, ],
- },
- 8: {
- 'L': [24, 2, 97, 0, 0, ],
- 'M': [22, 2, 38, 2, 39, ],
- 'Q': [22, 4, 18, 2, 19, ],
- 'H': [26, 4, 14, 2, 15, ],
- },
- 9: {
- 'L': [30, 2, 116, 0, 0, ],
- 'M': [22, 3, 36, 2, 37, ],
- 'Q': [20, 4, 16, 4, 17, ],
- 'H': [24, 4, 12, 4, 13, ],
- },
- 10: {
- 'L': [18, 2, 68, 2, 69, ],
- 'M': [26, 4, 43, 1, 44, ],
- 'Q': [24, 6, 19, 2, 20, ],
- 'H': [28, 6, 15, 2, 16, ],
- },
- 11: {
- 'L': [20, 4, 81, 0, 0, ],
- 'M': [30, 1, 50, 4, 51, ],
- 'Q': [28, 4, 22, 4, 23, ],
- 'H': [24, 3, 12, 8, 13, ],
- },
- 12: {
- 'L': [24, 2, 92, 2, 93, ],
- 'M': [22, 6, 36, 2, 37, ],
- 'Q': [26, 4, 20, 6, 21, ],
- 'H': [28, 7, 14, 4, 15, ],
- },
- 13: {
- 'L': [26, 4, 107, 0, 0, ],
- 'M': [22, 8, 37, 1, 38, ],
- 'Q': [24, 8, 20, 4, 21, ],
- 'H': [22, 12, 11, 4, 12, ],
- },
- 14: {
- 'L': [30, 3, 115, 1, 116, ],
- 'M': [24, 4, 40, 5, 41, ],
- 'Q': [20, 11, 16, 5, 17, ],
- 'H': [24, 11, 12, 5, 13, ],
- },
- 15: {
- 'L': [22, 5, 87, 1, 88, ],
- 'M': [24, 5, 41, 5, 42, ],
- 'Q': [30, 5, 24, 7, 25, ],
- 'H': [24, 11, 12, 7, 13, ],
- },
- 16: {
- 'L': [24, 5, 98, 1, 99, ],
- 'M': [28, 7, 45, 3, 46, ],
- 'Q': [24, 15, 19, 2, 20, ],
- 'H': [30, 3, 15, 13, 16, ],
- },
- 17: {
- 'L': [28, 1, 107, 5, 108, ],
- 'M': [28, 10, 46, 1, 47, ],
- 'Q': [28, 1, 22, 15, 23, ],
- 'H': [28, 2, 14, 17, 15, ],
- },
- 18: {
- 'L': [30, 5, 120, 1, 121, ],
- 'M': [26, 9, 43, 4, 44, ],
- 'Q': [28, 17, 22, 1, 23, ],
- 'H': [28, 2, 14, 19, 15, ],
- },
- 19: {
- 'L': [28, 3, 113, 4, 114, ],
- 'M': [26, 3, 44, 11, 45, ],
- 'Q': [26, 17, 21, 4, 22, ],
- 'H': [26, 9, 13, 16, 14, ],
- },
- 20: {
- 'L': [28, 3, 107, 5, 108, ],
- 'M': [26, 3, 41, 13, 42, ],
- 'Q': [30, 15, 24, 5, 25, ],
- 'H': [28, 15, 15, 10, 16, ],
- },
- 21: {
- 'L': [28, 4, 116, 4, 117, ],
- 'M': [26, 17, 42, 0, 0, ],
- 'Q': [28, 17, 22, 6, 23, ],
- 'H': [30, 19, 16, 6, 17, ],
- },
- 22: {
- 'L': [28, 2, 111, 7, 112, ],
- 'M': [28, 17, 46, 0, 0, ],
- 'Q': [30, 7, 24, 16, 25, ],
- 'H': [24, 34, 13, 0, 0, ],
- },
- 23: {
- 'L': [30, 4, 121, 5, 122, ],
- 'M': [28, 4, 47, 14, 48, ],
- 'Q': [30, 11, 24, 14, 25, ],
- 'H': [30, 16, 15, 14, 16, ],
- },
- 24: {
- 'L': [30, 6, 117, 4, 118, ],
- 'M': [28, 6, 45, 14, 46, ],
- 'Q': [30, 11, 24, 16, 25, ],
- 'H': [30, 30, 16, 2, 17, ],
- },
- 25: {
- 'L': [26, 8, 106, 4, 107, ],
- 'M': [28, 8, 47, 13, 48, ],
- 'Q': [30, 7, 24, 22, 25, ],
- 'H': [30, 22, 15, 13, 16, ],
- },
- 26: {
- 'L': [28, 10, 114, 2, 115, ],
- 'M': [28, 19, 46, 4, 47, ],
- 'Q': [28, 28, 22, 6, 23, ],
- 'H': [30, 33, 16, 4, 17, ],
- },
- 27: {
- 'L': [30, 8, 122, 4, 123, ],
- 'M': [28, 22, 45, 3, 46, ],
- 'Q': [30, 8, 23, 26, 24, ],
- 'H': [30, 12, 15, 28, 16, ],
- },
- 28: {
- 'L': [30, 3, 117, 10, 118, ],
- 'M': [28, 3, 45, 23, 46, ],
- 'Q': [30, 4, 24, 31, 25, ],
- 'H': [30, 11, 15, 31, 16, ],
- },
- 29: {
- 'L': [30, 7, 116, 7, 117, ],
- 'M': [28, 21, 45, 7, 46, ],
- 'Q': [30, 1, 23, 37, 24, ],
- 'H': [30, 19, 15, 26, 16, ],
- },
- 30: {
- 'L': [30, 5, 115, 10, 116, ],
- 'M': [28, 19, 47, 10, 48, ],
- 'Q': [30, 15, 24, 25, 25, ],
- 'H': [30, 23, 15, 25, 16, ],
- },
- 31: {
- 'L': [30, 13, 115, 3, 116, ],
- 'M': [28, 2, 46, 29, 47, ],
- 'Q': [30, 42, 24, 1, 25, ],
- 'H': [30, 23, 15, 28, 16, ],
- },
- 32: {
- 'L': [30, 17, 115, 0, 0, ],
- 'M': [28, 10, 46, 23, 47, ],
- 'Q': [30, 10, 24, 35, 25, ],
- 'H': [30, 19, 15, 35, 16, ],
- },
- 33: {
- 'L': [30, 17, 115, 1, 116, ],
- 'M': [28, 14, 46, 21, 47, ],
- 'Q': [30, 29, 24, 19, 25, ],
- 'H': [30, 11, 15, 46, 16, ],
- },
- 34: {
- 'L': [30, 13, 115, 6, 116, ],
- 'M': [28, 14, 46, 23, 47, ],
- 'Q': [30, 44, 24, 7, 25, ],
- 'H': [30, 59, 16, 1, 17, ],
- },
- 35: {
- 'L': [30, 12, 121, 7, 122, ],
- 'M': [28, 12, 47, 26, 48, ],
- 'Q': [30, 39, 24, 14, 25, ],
- 'H': [30, 22, 15, 41, 16, ],
- },
- 36: {
- 'L': [30, 6, 121, 14, 122, ],
- 'M': [28, 6, 47, 34, 48, ],
- 'Q': [30, 46, 24, 10, 25, ],
- 'H': [30, 2, 15, 64, 16, ],
- },
- 37: {
- 'L': [30, 17, 122, 4, 123, ],
- 'M': [28, 29, 46, 14, 47, ],
- 'Q': [30, 49, 24, 10, 25, ],
- 'H': [30, 24, 15, 46, 16, ],
- },
- 38: {
- 'L': [30, 4, 122, 18, 123, ],
- 'M': [28, 13, 46, 32, 47, ],
- 'Q': [30, 48, 24, 14, 25, ],
- 'H': [30, 42, 15, 32, 16, ],
- },
- 39: {
- 'L': [30, 20, 117, 4, 118, ],
- 'M': [28, 40, 47, 7, 48, ],
- 'Q': [30, 43, 24, 22, 25, ],
- 'H': [30, 10, 15, 67, 16, ],
- },
- 40: {
- 'L': [30, 19, 118, 6, 119, ],
- 'M': [28, 18, 47, 31, 48, ],
- 'Q': [30, 34, 24, 34, 25, ],
- 'H': [30, 20, 15, 61, 16, ],
- },
-}
-
-#: This table lists all of the generator polynomials used by QR Codes.
-#: They are indexed by the number of "ECC Code Words" (see table above).
-#: This table is taken from:
-#:
-#: http://www.matchadesign.com/blog/qr-code-demystified-part-4/
-generator_polynomials = {
- 7:[87, 229, 146, 149, 238, 102, 21],
- 10:[251, 67, 46, 61, 118, 70, 64, 94, 32, 45],
- 13:[74, 152, 176, 100, 86, 100, 106, 104, 130, 218, 206, 140, 78],
- 15:[8, 183, 61, 91, 202, 37, 51, 58, 58, 237, 140, 124, 5, 99, 105],
- 16:[120, 104, 107, 109, 102, 161, 76, 3, 91, 191, 147, 169, 182, 194,
- 225, 120],
- 17:[43, 139, 206, 78, 43, 239, 123, 206, 214, 147, 24, 99, 150, 39,
- 243, 163, 136],
- 18:[215, 234, 158, 94, 184, 97, 118, 170, 79, 187, 152, 148, 252, 179,
- 5, 98, 96, 153],
- 20:[17, 60, 79, 50, 61, 163, 26, 187, 202, 180, 221, 225, 83, 239, 156,
- 164, 212, 212, 188, 190],
- 22:[210, 171, 247, 242, 93, 230, 14, 109, 221, 53, 200, 74, 8, 172, 98,
- 80, 219, 134, 160, 105, 165, 231],
- 24:[229, 121, 135, 48, 211, 117, 251, 126, 159, 180, 169, 152, 192, 226,
- 228, 218, 111, 0, 117, 232, 87, 96, 227, 21],
- 26:[173, 125, 158, 2, 103, 182, 118, 17, 145, 201, 111, 28, 165, 53, 161,
- 21, 245, 142, 13, 102, 48, 227, 153, 145, 218, 70],
- 28:[168, 223, 200, 104, 224, 234, 108, 180, 110, 190, 195, 147, 205, 27,
- 232, 201, 21, 43, 245, 87, 42, 195, 212, 119, 242, 37, 9, 123],
- 30:[41, 173, 145, 152, 216, 31, 179, 182, 50, 48, 110, 86, 239, 96, 222,
- 125, 42, 173, 226, 193, 224, 130, 156, 37, 251, 216, 238, 40, 192,
- 180]
-}
-
-#: This table contains the log and values used in GF(256) arithmetic.
-#: They are used to generate error correction codes for QR Codes.
-#: This table is taken from:
-#:
-#: vhttp://www.thonky.com/qr-code-tutorial/log-antilog-table/
-galois_log = [
- 1, 2, 4, 8, 16, 32, 64, 128, 29, 58, 116, 232, 205, 135, 19, 38, 76, 152,
- 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48, 96, 192, 157, 39, 78,
- 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35, 70, 140, 5,
- 10, 20, 40, 80, 160, 93, 186, 105, 210, 185, 111, 222, 161, 95, 190, 97,
- 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240, 253, 231, 211,
- 187, 107, 214, 177, 127, 254, 225, 223, 163, 91, 182, 113, 226, 217, 175,
- 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208, 189, 103, 206, 129, 31,
- 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204, 133,
- 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84, 168, 77,
- 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115, 230, 209,
- 191, 99, 198, 145, 63, 126, 252, 229, 215, 179, 123, 246, 241, 255, 227,
- 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65, 130,
- 25, 50, 100, 200, 141, 7, 14, 28, 56, 112, 224, 221, 167, 83, 166, 81,
- 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9, 18,
- 36, 72, 144, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22, 44,
- 88, 176, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1,]
-
-#: This table contains the antilog and values used in GF(256) arithmetic.
-#: They are used to generate error correction codes for QR Codes.
-#: This table is taken from:
-#:
-#: http://www.thonky.com/qr-code-tutorial/log-antilog-table/
-galois_antilog = [
- None, 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 4, 100,
- 224, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, 5, 138,
- 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69, 29, 181,
- 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, 6, 191,
- 139, 98, 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136, 54, 208,
- 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64, 30, 66,
- 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61, 202, 94,
- 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, 7, 112,
- 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24, 227, 165,
- 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46, 55, 63,
- 209, 91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97, 242,
- 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, 109, 65, 162, 31, 45,
- 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246, 108,
- 161, 59, 82, 41, 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90, 203,
- 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122, 117, 44, 215, 79,
- 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175,]
-
-#: This table contains the coordinates for the position adjustment patterns.
-#: The index of the table corresponds to the QR Code's version number.
-#: This table is taken from:
-#:
-#: http://www.thonky.com/qr-code-tutorial/part-3-mask-pattern/
-position_adjustment = [
- None, #There is not version 0
- None, #Version 1 does not need adjustment
- [6, 18, ],
- [6, 22, ],
- [6, 26, ],
- [6, 30, ],
- [6, 34, ],
- [6, 22, 38, ],
- [6, 24, 42, ],
- [6, 26, 46, ],
- [6, 28, 50, ],
- [6, 30, 54, ],
- [6, 32, 58, ],
- [6, 34, 62, ],
- [6, 26, 46, 66, ],
- [6, 26, 48, 70, ],
- [6, 26, 50, 74, ],
- [6, 30, 54, 78, ],
- [6, 30, 56, 82, ],
- [6, 30, 58, 86, ],
- [6, 34, 62, 90, ],
- [6, 28, 50, 72, 94, ],
- [6, 26, 50, 74, 98, ],
- [6, 30, 54, 78, 102, ],
- [6, 28, 54, 80, 106, ],
- [6, 32, 58, 84, 110, ],
- [6, 30, 58, 86, 114, ],
- [6, 34, 62, 90, 118, ],
- [6, 26, 50, 74, 98, 122, ],
- [6, 30, 54, 78, 102, 126, ],
- [6, 26, 52, 78, 104, 130, ],
- [6, 30, 56, 82, 108, 134, ],
- [6, 34, 60, 86, 112, 138, ],
- [6, 30, 58, 86, 114, 142, ],
- [6, 34, 62, 90, 118, 146, ],
- [6, 30, 54, 78, 102, 126, 150, ],
- [6, 24, 50, 76, 102, 128, 154, ],
- [6, 28, 54, 80, 106, 132, 158, ],
- [6, 32, 58, 84, 110, 136, 162, ],
- [6, 26, 54, 82, 110, 138, 166, ],
- [6, 30, 58, 86, 114, 142, 170, ],
-]
-
-#: This table specifies the bit pattern to be added to a QR Code's
-#: image to specify what version the code is. Note, this pattern
-#: is not used for versions 1-6. This table is taken from:
-#:
-#: http://www.thonky.com/qr-code-tutorial/part-3-mask-pattern/
-version_pattern = [None,None,None,None,None,None,None, #0-6
- '000111110010010100', '001000010110111100', '001001101010011001',
- '001010010011010011', '001011101111110110', '001100011101100010',
- '001101100001000111', '001110011000001101', '001111100100101000',
- '010000101101111000', '010001010001011101', '010010101000010111',
- '010011010100110010', '010100100110100110', '010101011010000011',
- '010110100011001001', '010111011111101100', '011000111011000100',
- '011001000111100001', '011010111110101011', '011011000010001110',
- '011100110000011010', '011101001100111111', '011110110101110101',
- '011111001001010000', '100000100111010101', '100001011011110000',
- '100010100010111010', '100011011110011111', '100100101100001011',
- '100101010000101110', '100110101001100100', '100111010101000001',
- '101000110001101001'
-]
-
-#: This table contains the bit fields needed to specify the error code level and
-#: mask pattern used by a QR Code. This table is take from:
-#:
-#: http://www.thonky.com/qr-code-tutorial/part-3-mask-pattern/
-type_bits = {
- 'L':{
- 0:'111011111000100',
- 1:'111001011110011',
- 2:'111110110101010',
- 3:'111100010011101',
- 4:'110011000101111',
- 5:'110001100011000',
- 6:'110110001000001',
- 7:'110100101110110',
- },
- 'M':{
- 0:'101010000010010',
- 1:'101000100100101',
- 2:'101111001111100',
- 3:'101101101001011',
- 4:'100010111111001',
- 5:'100000011001110',
- 6:'100111110010111',
- 7:'100101010100000',
- },
- 'Q':{
- 0:'011010101011111',
- 1:'011000001101000',
- 2:'011111100110001',
- 3:'011101000000110',
- 4:'010010010110100',
- 5:'010000110000011',
- 6:'010111011011010',
- 7:'010101111101101',
- },
- 'H':{
- 0:'001011010001001',
- 1:'001001110111110',
- 2:'001110011100111',
- 3:'001100111010000',
- 4:'000011101100010',
- 5:'000001001010101',
- 6:'000110100001100',
- 7:'000100000111011',
- },
-}
-
-#: This table contains *functions* to compute whether to change current bit when
-#: creating the masks. All of the functions in the table return a boolean value.
-#: A True result means you should add the bit to the QR Code exactly as is. A
-#: False result means you should add the opposite bit. This table was taken
-#: from:
-#:
-#: http://www.thonky.com/qr-code-tutorial/mask-patterns/
-mask_patterns = [
- lambda row,col: (row + col) % 2 == 0,
- lambda row,col: row % 2 == 0,
- lambda row,col: col % 3 == 0,
- lambda row,col: (row + col) % 3 == 0,
- lambda row,col: ((row // 2) + (col // 3)) % 2 == 0,
- lambda row,col: ((row * col) % 2) + ((row * col) % 3) == 0,
- lambda row,col: (((row * col) % 2) + ((row * col) % 3)) % 2 == 0,
- lambda row,col: (((row + col) % 2) + ((row * col) % 3)) % 2 == 0]
diff --git a/pysimplesoap/__init__.py b/pysimplesoap/__init__.py
deleted file mode 100644
index 271cba3..0000000
--- a/pysimplesoap/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""PySimpleSOAP"""
-
-
-__author__ = "Mariano Reingart"
-__author_email__ = "reingart@gmail.com"
-__copyright__ = "Copyright (C) 2013 Mariano Reingart"
-__license__ = "LGPL 3.0"
-__version__ = "1.11"
-
-TIMEOUT = 30
-
-
-from . import client, server, simplexml, transport
diff --git a/pysimplesoap/client.py b/pysimplesoap/client.py
deleted file mode 100644
index ae06884..0000000
--- a/pysimplesoap/client.py
+++ /dev/null
@@ -1,713 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by the
-# Free Software Foundation; either version 3, 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 MERCHANTIBILITY
-# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
-# for more details.
-
-"""Pythonic simple SOAP Client implementation"""
-
-from __future__ import unicode_literals
-import sys
-if sys.version > '3':
- unicode = str
-
-try:
- import cPickle as pickle
-except ImportError:
- import pickle
-import hashlib
-import logging
-import os
-import tempfile
-
-from . import __author__, __copyright__, __license__, __version__, TIMEOUT
-from .simplexml import SimpleXMLElement, TYPE_MAP, REVERSE_TYPE_MAP, OrderedDict
-from .transport import get_http_wrapper, set_http_wrapper, get_Http
-# Utility functions used throughout wsdl_parse, moved aside for readability
-from .helpers import fetch, sort_dict, make_key, process_element, \
- postprocess_element, get_message, preprocess_schema, \
- get_local_name, get_namespace_prefix, TYPE_MAP
-
-
-log = logging.getLogger(__name__)
-
-
-class SoapFault(RuntimeError):
- def __init__(self, faultcode, faultstring):
- self.faultcode = faultcode
- self.faultstring = faultstring
- RuntimeError.__init__(self, faultcode, faultstring)
-
- def __str__(self):
- return self.__unicode__().encode('ascii', 'ignore')
-
- def __unicode__(self):
- return '%s: %s' % (self.faultcode, self.faultstring)
-
- def __repr__(self):
- return "SoapFault(%s, %s)" % (repr(self.faultcode),
- repr(self.faultstring))
-
-
-# soap protocol specification & namespace
-soap_namespaces = dict(
- soap11='http://schemas.xmlsoap.org/soap/envelope/',
- soap='http://schemas.xmlsoap.org/soap/envelope/',
- soapenv='http://schemas.xmlsoap.org/soap/envelope/',
- soap12='http://www.w3.org/2003/05/soap-env',
- soap12env="http://www.w3.org/2003/05/soap-envelope",
-)
-
-
-class SoapClient(object):
- """Simple SOAP Client (simil PHP)"""
- def __init__(self, location=None, action=None, namespace=None,
- cert=None, exceptions=True, proxy=None, ns=None,
- soap_ns=None, wsdl=None, wsdl_basedir='', cache=False, cacert=None,
- sessions=False, soap_server=None, timeout=TIMEOUT,
- http_headers=None, trace=False,
- ):
- """
- :param http_headers: Additional HTTP Headers; example: {'Host': 'ipsec.example.com'}
- """
- self.certssl = cert
- self.keyssl = None
- self.location = location # server location (url)
- self.action = action # SOAP base action
- self.namespace = namespace # message
- self.exceptions = exceptions # lanzar execpiones? (Soap Faults)
- self.xml_request = self.xml_response = ''
- self.http_headers = http_headers or {}
- self.wsdl_basedir = wsdl_basedir
-
- # shortcut to print all debugging info and sent / received xml messages
- if trace:
- logging.basicConfig(level=logging.DEBUG)
-
- if not soap_ns and not ns:
- self.__soap_ns = 'soap' # 1.1
- elif not soap_ns and ns:
- self.__soap_ns = 'soapenv' # 1.2
- else:
- self.__soap_ns = soap_ns
-
- # SOAP Server (special cases like oracle, jbossas6 or jetty)
- self.__soap_server = soap_server
-
- # SOAP Header support
- self.__headers = {} # general headers
- self.__call_headers = None # OrderedDict to be marshalled for RPC Call
-
- # check if the Certification Authority Cert is a string and store it
- if cacert and cacert.startswith('-----BEGIN CERTIFICATE-----'):
- fd, filename = tempfile.mkstemp()
- f = os.fdopen(fd, 'w+b', -1)
- log.debug("Saving CA certificate to %s" % filename)
- f.write(cacert)
- cacert = filename
- f.close()
- self.cacert = cacert
-
- # Create HTTP wrapper
- Http = get_Http()
- self.http = Http(timeout=timeout, cacert=cacert, proxy=proxy, sessions=sessions)
-
- # namespace prefix, None to use xmlns attribute or False to not use it:
- self.__ns = ns
- if not ns:
- self.__xml = """
-<%(soap_ns)s:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema"
- xmlns:%(soap_ns)s="%(soap_uri)s">
-<%(soap_ns)s:Header/>
-<%(soap_ns)s:Body>
- <%(method)s xmlns="%(namespace)s">
- %(method)s>
-%(soap_ns)s:Body>
-%(soap_ns)s:Envelope>"""
- else:
- self.__xml = """
-<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s" xmlns:%(ns)s="%(namespace)s">
-<%(soap_ns)s:Header/>
-<%(soap_ns)s:Body>
- <%(ns)s:%(method)s>
- %(ns)s:%(method)s>
-%(soap_ns)s:Body>
-%(soap_ns)s:Envelope>"""
-
- # parse wsdl url
- self.services = wsdl and self.wsdl_parse(wsdl, cache=cache)
- self.service_port = None # service port for late binding
-
- def __getattr__(self, attr):
- """Return a pseudo-method that can be called"""
- if not self.services: # not using WSDL?
- return lambda self=self, *args, **kwargs: self.call(attr, *args, **kwargs)
- else: # using WSDL:
- return lambda *args, **kwargs: self.wsdl_call(attr, *args, **kwargs)
-
- def call(self, method, *args, **kwargs):
- """Prepare xml request and make SOAP call, returning a SimpleXMLElement.
-
- If a keyword argument called "headers" is passed with a value of a
- SimpleXMLElement object, then these headers will be inserted into the
- request.
- """
- #TODO: method != input_message
- # Basic SOAP request:
- xml = self.__xml % dict(method=method, # method tag name
- namespace=self.namespace, # method ns uri
- ns=self.__ns, # method ns prefix
- soap_ns=self.__soap_ns, # soap prefix & uri
- soap_uri=soap_namespaces[self.__soap_ns])
- request = SimpleXMLElement(xml, namespace=self.__ns and self.namespace,
- prefix=self.__ns)
-
- request_headers = kwargs.pop('headers', None)
-
- # serialize parameters
- if kwargs:
- parameters = list(kwargs.items())
- else:
- parameters = args
- if parameters and isinstance(parameters[0], SimpleXMLElement):
- # merge xmlelement parameter ("raw" - already marshalled)
- if parameters[0].children() is not None:
- for param in parameters[0].children():
- getattr(request, method).import_node(param)
- elif parameters:
- # marshall parameters:
- use_ns = None if self.__soap_server == "jetty" else True
- for k, v in parameters: # dict: tag=valor
- getattr(request, method).marshall(k, v, ns=use_ns)
- elif not self.__soap_server in ('oracle',) or self.__soap_server in ('jbossas6',):
- # JBossAS-6 requires no empty method parameters!
- delattr(request("Body", ns=list(soap_namespaces.values()),), method)
-
- # construct header and parameters (if not wsdl given) except wsse
- if self.__headers and not self.services:
- self.__call_headers = dict([(k, v) for k, v in self.__headers.items()
- if not k.startswith('wsse:')])
- # always extract WS Security header and send it
- if 'wsse:Security' in self.__headers:
- #TODO: namespaces too hardwired, clean-up...
- header = request('Header', ns=list(soap_namespaces.values()),)
- k = 'wsse:Security'
- v = self.__headers[k]
- header.marshall(k, v, ns=False, add_children_ns=False)
- header(k)['xmlns:wsse'] = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
- #
- if self.__call_headers:
- header = request('Header', ns=list(soap_namespaces.values()),)
- for k, v in self.__call_headers.items():
- ##if not self.__ns:
- ## header['xmlns']
- if isinstance(v, SimpleXMLElement):
- # allows a SimpleXMLElement to be constructed and inserted
- # rather than a dictionary. marshall doesn't allow ns: prefixes
- # in dict key names
- header.import_node(v)
- else:
- header.marshall(k, v, ns=self.__ns, add_children_ns=False)
- if request_headers:
- header = request('Header', ns=list(soap_namespaces.values()),)
- for subheader in request_headers.children():
- header.import_node(subheader)
-
- self.xml_request = request.as_xml()
- self.xml_response = self.send(method, self.xml_request)
- response = SimpleXMLElement(self.xml_response, namespace=self.namespace,
- jetty=self.__soap_server in ('jetty',))
- if self.exceptions and response("Fault", ns=list(soap_namespaces.values()), error=False):
- raise SoapFault(unicode(response.faultcode), unicode(response.faultstring))
- return response
-
- def send(self, method, xml):
- """Send SOAP request using HTTP"""
- if self.location == 'test': return
- # location = '%s' % self.location #?op=%s" % (self.location, method)
- location = str(self.location)
-
- if self.services:
- soap_action = str(self.action)
- else:
- soap_action = str(self.action) + method
-
- headers = {
- 'Content-type': 'text/xml; charset="UTF-8"',
- 'Content-length': str(len(xml)),
- 'SOAPAction': '"%s"' % soap_action
- }
- headers.update(self.http_headers)
- log.info("POST %s" % location)
- log.debug('\n'.join(["%s: %s" % (k, v) for k, v in headers.items()]))
- log.debug(xml)
-
- response, content = self.http.request(
- location, 'POST', body=xml, headers=headers)
- self.response = response
- self.content = content
-
- log.debug('\n'.join(["%s: %s" % (k, v) for k, v in response.items()]))
- log.debug(content)
- return content
-
- def get_operation(self, method):
- # try to find operation in wsdl file
- soap_ver = self.__soap_ns.startswith('soap12') and 'soap12' or 'soap11'
- if not self.service_port:
- for service_name, service in self.services.items():
- for port_name, port in [port for port in service['ports'].items()]:
- if port['soap_ver'] == soap_ver:
- self.service_port = service_name, port_name
- break
- else:
- raise RuntimeError('Cannot determine service in WSDL: '
- 'SOAP version: %s' % soap_ver)
- else:
- port = self.services[self.service_port[0]]['ports'][self.service_port[1]]
- if not self.location:
- self.location = port['location']
- operation = port['operations'].get(method)
- if not operation:
- raise RuntimeError('Operation %s not found in WSDL: '
- 'Service/Port Type: %s' %
- (method, self.service_port))
- return operation
-
- def wsdl_call(self, method, *args, **kwargs):
- """Pre and post process SOAP call, input and output parameters using WSDL"""
- soap_uri = soap_namespaces[self.__soap_ns]
- operation = self.get_operation(method)
-
- # get i/o type declarations:
- input = operation['input']
- output = operation['output']
- header = operation.get('header')
- if 'action' in operation:
- self.action = operation['action']
-
- if 'namespace' in operation:
- self.namespace = operation['namespace']
-
- # construct header and parameters
- if header:
- self.__call_headers = sort_dict(header, self.__headers)
- method, params = self.wsdl_call_get_params(method, input, *args, **kwargs)
-
- # call remote procedure
- response = self.call(method, *params)
- # parse results:
- resp = response('Body', ns=soap_uri).children().unmarshall(output)
- return resp and list(resp.values())[0] # pass Response tag children
-
- def wsdl_call_get_params(self, method, input, *args, **kwargs):
- """Build params from input and args/kwargs"""
- params = inputname = inputargs = None
- all_args = {}
- if input:
- inputname = list(input.keys())[0]
- inputargs = input[inputname]
-
- if input and args:
- # convert positional parameters to named parameters:
- d = {}
- for idx, arg in enumerate(args):
- key = list(inputargs.keys())[idx]
- if isinstance(arg, dict):
- if key in arg:
- d[key] = arg[key]
- else:
- raise KeyError('Unhandled key %s. use client.help(method)')
- else:
- d[key] = arg
- all_args.update({inputname: d})
-
- if input and (kwargs or all_args):
- if kwargs:
- all_args.update({inputname: kwargs})
- valid, errors, warnings = self.wsdl_validate_params(input, all_args)
- if not valid:
- raise ValueError('Invalid Args Structure. Errors: %s' % errors)
- params = list(sort_dict(input, all_args).values())[0].items()
- if self.__soap_server == 'axis':
- # use the operation name
- method = method
- else:
- # use the message (element) name
- method = inputname
- #elif not input:
- #TODO: no message! (see wsmtxca.dummy)
- else:
- params = kwargs and kwargs.items()
-
- return (method, params)
-
- def wsdl_validate_params(self, struct, value):
- """Validate the arguments (actual values) for the parameters structure.
- Fail for any invalid arguments or type mismatches."""
- errors = []
- warnings = []
- valid = True
-
- # Determine parameter type
- if type(struct) == type(value):
- typematch = True
- if not isinstance(struct, dict) and isinstance(value, dict):
- typematch = True # struct can be an OrderedDict
- else:
- typematch = False
-
- if struct == str:
- struct = unicode # fix for py2 vs py3 string handling
-
- if not isinstance(struct, (list, dict, tuple)) and struct in TYPE_MAP.keys():
- try:
- struct(value) # attempt to cast input to parameter type
- except:
- valid = False
- errors.append('Type mismatch for argument value. parameter(%s): %s, value(%s): %s' % (type(struct), struct, type(value), value))
-
- elif isinstance(struct, list) and len(struct) == 1 and not isinstance(value, list):
- # parameter can have a dict in a list: [{}] indicating a list is allowed, but not needed if only one argument.
- next_valid, next_errors, next_warnings = self.wsdl_validate_params(struct[0], value)
- if not next_valid:
- valid = False
- errors.extend(next_errors)
- warnings.extend(next_warnings)
-
- # traverse tree
- elif isinstance(struct, dict):
- if struct and value:
- for key in value:
- if key not in struct:
- valid = False
- errors.append('Argument key %s not in parameter. parameter: %s, args: %s' % (key, struct, value))
- else:
- next_valid, next_errors, next_warnings = self.wsdl_validate_params(struct[key], value[key])
- if not next_valid:
- valid = False
- errors.extend(next_errors)
- warnings.extend(next_warnings)
- for key in struct:
- if key not in value:
- warnings.append('Parameter key %s not in args. parameter: %s, value: %s' % (key, struct, value))
- elif struct and not value:
- warnings.append('parameter keys not in args. parameter: %s, args: %s' % (struct, value))
- elif not struct and value:
- valid = False
- errors.append('Args keys not in parameter. parameter: %s, args: %s' % (struct, value))
- else:
- pass
- elif isinstance(struct, list):
- struct_list_value = struct[0]
- for item in value:
- next_valid, next_errors, next_warnings = self.wsdl_validate_params(struct_list_value, item)
- if not next_valid:
- valid = False
- errors.extend(next_errors)
- warnings.extend(next_warnings)
- elif not typematch:
- valid = False
- errors.append('Type mismatch. parameter(%s): %s, value(%s): %s' % (type(struct), struct, type(value), value))
-
- return (valid, errors, warnings)
-
- def help(self, method):
- """Return operation documentation and invocation/returned value example"""
- operation = self.get_operation(method)
- input = operation.get('input')
- input = input and input.values() and list(input.values())[0]
- if isinstance(input, dict):
- input = ", ".join("%s=%s" % (k, repr(v)) for k, v in input.items())
- elif isinstance(input, list):
- input = repr(input)
- output = operation.get('output')
- if output:
- output = list(operation['output'].values())[0]
- headers = operation.get('headers') or None
- return "%s(%s)\n -> %s:\n\n%s\nHeaders: %s" % (
- method,
- input or '',
- output and output or '',
- operation.get('documentation', ''),
- headers,
- )
-
- def wsdl_parse(self, url, cache=False):
- """Parse Web Service Description v1.1"""
-
- log.debug('Parsing wsdl url: %s' % url)
- # Try to load a previously parsed wsdl:
- force_download = False
- if cache:
- # make md5 hash of the url for caching...
- filename_pkl = '%s.pkl' % hashlib.md5(url.encode('utf-8')).hexdigest()
- if isinstance(cache, str):
- filename_pkl = os.path.join(cache, filename_pkl)
- if os.path.exists(filename_pkl):
- log.debug('Unpickle file %s' % (filename_pkl, ))
- f = open(filename_pkl, 'rb')
- pkl = pickle.load(f)
- f.close()
- # sanity check:
- if pkl['version'][:-1] != __version__.split(' ')[0][:-1] or pkl['url'] != url:
- import warnings
- warnings.warn('version or url mismatch! discarding cached wsdl', RuntimeWarning)
- log.debug('Version: %s %s' % (pkl['version'], __version__))
- log.debug('URL: %s %s' % (pkl['url'], url))
- force_download = True
- else:
- self.namespace = pkl['namespace']
- self.documentation = pkl['documentation']
- return pkl['services']
-
- soap_ns = {
- 'http://schemas.xmlsoap.org/wsdl/soap/': 'soap11',
- 'http://schemas.xmlsoap.org/wsdl/soap12/': 'soap12',
- }
- wsdl_uri = 'http://schemas.xmlsoap.org/wsdl/'
- xsd_uri = 'http://www.w3.org/2001/XMLSchema'
- xsi_uri = 'http://www.w3.org/2001/XMLSchema-instance'
-
- # always return an unicode object:
- REVERSE_TYPE_MAP['string'] = str
-
- # Open uri and read xml:
- xml = fetch(url, self.http, cache, force_download, self.wsdl_basedir)
- # Parse WSDL XML:
- wsdl = SimpleXMLElement(xml, namespace=wsdl_uri)
-
- # Extract useful data:
- self.namespace = None
- self.documentation = unicode(wsdl('documentation', error=False)) or ''
-
- # some wsdl are splitted down in several files, join them:
- imported_wsdls = {}
- for element in wsdl.children() or []:
- if element.get_local_name() in ('import'):
- wsdl_namespace = element['namespace']
- wsdl_location = element['location']
- if wsdl_location is None:
- log.warning('WSDL location not provided for %s!' % wsdl_namespace)
- continue
- if wsdl_location in imported_wsdls:
- log.warning('WSDL %s already imported!' % wsdl_location)
- continue
- imported_wsdls[wsdl_location] = wsdl_namespace
- log.debug('Importing wsdl %s from %s' % (wsdl_namespace, wsdl_location))
- # Open uri and read xml:
- xml = fetch(wsdl_location, self.http, cache, force_download, self.wsdl_basedir)
- # Parse imported XML schema (recursively):
- imported_wsdl = SimpleXMLElement(xml, namespace=xsd_uri)
- # merge the imported wsdl into the main document:
- wsdl.import_node(imported_wsdl)
- # warning: do not process schemas to avoid infinite recursion!
-
-
- # detect soap prefix and uri (xmlns attributes of )
- xsd_ns = None
- soap_uris = {}
- for k, v in wsdl[:]:
- if v in soap_ns and k.startswith('xmlns:'):
- soap_uris[get_local_name(k)] = v
- if v == xsd_uri and k.startswith('xmlns:'):
- xsd_ns = get_local_name(k)
-
- services = {}
- bindings = {} # binding_name: binding
- operations = {} # operation_name: operation
- port_type_bindings = {} # port_type_name: binding
- messages = {} # message: element
- elements = {} # element: type def
-
- for service in wsdl.service:
- service_name = service['name']
- if not service_name:
- continue # empty service?
- serv = services.setdefault(service_name, {'ports': {}})
- serv['documentation'] = service['documentation'] or ''
- for port in service.port:
- binding_name = get_local_name(port['binding'])
- operations[binding_name] = {}
- address = port('address', ns=list(soap_uris.values()), error=False)
- location = address and address['location'] or None
- soap_uri = address and soap_uris.get(address.get_prefix())
- soap_ver = soap_uri and soap_ns.get(soap_uri)
- bindings[binding_name] = {'name': binding_name,
- 'service_name': service_name,
- 'location': location,
- 'soap_uri': soap_uri,
- 'soap_ver': soap_ver, }
- serv['ports'][port['name']] = bindings[binding_name]
-
- for binding in wsdl.binding:
- binding_name = binding['name']
- soap_binding = binding('binding', ns=list(soap_uris.values()), error=False)
- transport = soap_binding and soap_binding['transport'] or None
- port_type_name = get_local_name(binding['type'])
- bindings[binding_name].update({
- 'port_type_name': port_type_name,
- 'transport': transport, 'operations': {},
- })
- if port_type_name not in port_type_bindings:
- port_type_bindings[port_type_name] = []
- port_type_bindings[port_type_name].append(bindings[binding_name])
- for operation in binding.operation:
- op_name = operation['name']
- op = operation('operation', ns=list(soap_uris.values()), error=False)
- action = op and op['soapAction']
- d = operations[binding_name].setdefault(op_name, {})
- bindings[binding_name]['operations'][op_name] = d
- d.update({'name': op_name})
- d['parts'] = {}
- # input and/or ouput can be not present!
- input = operation('input', error=False)
- body = input and input('body', ns=list(soap_uris.values()), error=False)
- d['parts']['input_body'] = body and body['parts'] or None
- output = operation('output', error=False)
- body = output and output('body', ns=list(soap_uris.values()), error=False)
- d['parts']['output_body'] = body and body['parts'] or None
- header = input and input('header', ns=list(soap_uris.values()), error=False)
- d['parts']['input_header'] = header and {'message': header['message'], 'part': header['part']} or None
- header = output and output('header', ns=list(soap_uris.values()), error=False)
- d['parts']['output_header'] = header and {'message': header['message'], 'part': header['part']} or None
- if action:
- d['action'] = action
-
- # check axis2 namespace at schema types attributes (europa.eu checkVat)
- if "http://xml.apache.org/xml-soap" in dict(wsdl[:]).values():
- # get the sub-namespace in the first schema element (see issue 8)
- schema = wsdl.types('schema', ns=xsd_uri)
- attrs = dict(schema[:])
- self.namespace = attrs.get('targetNamespace', self.namespace)
-
- imported_schemas = {}
- global_namespaces = {}
-
- # process current wsdl schema:
- for schema in wsdl.types('schema', ns=xsd_uri):
- preprocess_schema(schema, imported_schemas, elements, xsd_uri, self.__soap_server, self.http, cache, force_download, self.wsdl_basedir, global_namespaces=global_namespaces)
-
- postprocess_element(elements)
-
- for message in wsdl.message:
- for part in message('part', error=False) or []:
- element = {}
- element_name = part['element']
- if not element_name:
- # some implementations (axis) uses type instead
- element_name = part['type']
- type_ns = get_namespace_prefix(element_name)
- type_uri = wsdl.get_namespace_uri(type_ns)
- if type_uri == xsd_uri:
- element_name = get_local_name(element_name)
- fn = REVERSE_TYPE_MAP.get(element_name, None)
- element = {part['name']: fn}
- # emulate a true Element (complexType)
- list(messages.setdefault((message['name'], None), {message['name']: OrderedDict()}).values())[0].update(element)
- else:
- element_name = get_local_name(element_name)
- fn = elements.get(make_key(element_name, 'element', type_uri))
- if not fn:
- # some axis servers uses complexType for part messages
- fn = elements.get(make_key(element_name, 'complexType', type_uri))
- element = {message['name']: {part['name']: fn}}
- else:
- element = {element_name: fn}
- messages[(message['name'], part['name'])] = element
-
- for port_type in wsdl.portType:
- port_type_name = port_type['name']
-
- for binding in port_type_bindings.get(port_type_name, []):
- for operation in port_type.operation:
- op_name = operation['name']
- op = operations[binding['name']][op_name]
- op['documentation'] = unicode(operation('documentation', error=False)) or ''
- if binding['soap_ver']:
- #TODO: separe operation_binding from operation (non SOAP?)
- if operation('input', error=False):
- input_msg = get_local_name(operation.input['message'])
- input_header = op['parts'].get('input_header')
- if input_header:
- header_msg = get_local_name(input_header.get('message'))
- header_part = get_local_name(input_header.get('part'))
- # warning: some implementations use a separate message!
- header = get_message(messages, header_msg or input_msg, header_part)
- else:
- header = None # not enought info to search the header message:
- op['input'] = get_message(messages, input_msg, op['parts'].get('input_body'))
- op['header'] = header
- try:
- ns_uri = list(op['input'].values())[0].namespace
- except AttributeError:
- # TODO: fix if no parameters parsed or "variants"
- ns = get_namespace_prefix(operation.input['message'])
- ns_uri = operation.get_namespace_uri(ns)
- if ns_uri:
- op['namespace'] = ns_uri
- else:
- op['input'] = None
- op['header'] = None
- if operation('output', error=False):
- output_msg = get_local_name(operation.output['message'])
- op['output'] = get_message(messages, output_msg, op['parts'].get('output_body'))
- else:
- op['output'] = None
-
- # dump the full service/port/operation map
- #log.debug(pprint.pformat(services))
-
- # Save parsed wsdl (cache)
- if cache:
- f = open(filename_pkl, "wb")
- pkl = {
- 'version': __version__.split(' ')[0],
- 'url': url,
- 'namespace': self.namespace,
- 'documentation': self.documentation,
- 'services': services,
- }
- pickle.dump(pkl, f)
- f.close()
-
- return services
-
- def __setitem__(self, item, value):
- """Set SOAP Header value - this header will be sent for every request."""
- self.__headers[item] = value
-
- def close(self):
- """Finish the connection and remove temp files"""
- self.http.close()
- if self.cacert.startswith(tempfile.gettempdir()):
- log.debug('removing %s' % self.cacert)
- os.unlink(self.cacert)
-
-
-def parse_proxy(proxy_str):
- """Parses proxy address user:pass@host:port into a dict suitable for httplib2"""
- proxy_dict = {}
- if proxy_str is None:
- return
- if '@' in proxy_str:
- user_pass, host_port = proxy_str.split('@')
- else:
- user_pass, host_port = '', proxy_str
- if ':' in host_port:
- host, port = host_port.split(':')
- proxy_dict['proxy_host'], proxy_dict['proxy_port'] = host, int(port)
- if ':' in user_pass:
- proxy_dict['proxy_user'], proxy_dict['proxy_pass'] = user_pass.split(':')
- return proxy_dict
-
-
-if __name__ == '__main__':
- pass
diff --git a/pysimplesoap/helpers.py b/pysimplesoap/helpers.py
deleted file mode 100644
index 434a886..0000000
--- a/pysimplesoap/helpers.py
+++ /dev/null
@@ -1,465 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by the
-# Free Software Foundation; either version 3, 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 MERCHANTIBILITY
-# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
-# for more details.
-
-"""Pythonic simple SOAP Client helpers"""
-
-
-from __future__ import unicode_literals
-import sys
-if sys.version > '3':
- basestring = unicode = str
-
-import datetime
-from decimal import Decimal
-import os
-import logging
-import hashlib
-import warnings
-
-try:
- import urllib2
- from urlparse import urlsplit
-except ImportError:
- from urllib import request as urllib2
- from urllib.parse import urlsplit
-
-from . import __author__, __copyright__, __license__, __version__
-
-
-log = logging.getLogger(__name__)
-
-
-def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''):
- """Download a document from a URL, save it locally if cache enabled"""
-
- # check / append a valid schema if not given:
- url_scheme, netloc, path, query, fragment = urlsplit(url)
- if not url_scheme in ('http', 'https', 'file'):
- for scheme in ('http', 'https', 'file'):
- try:
- if not url.startswith("/") and scheme in ('http', 'https'):
- tmp_url = "%s://%s" % (scheme, os.path.join(wsdl_basedir, url))
- else:
- tmp_url = "%s:%s" % (scheme, os.path.join(wsdl_basedir, url))
- log.debug('Scheme not found, trying %s' % scheme)
- return fetch(tmp_url, http, cache, force_download, wsdl_basedir)
- except Exception as e:
- log.error(e)
- raise RuntimeError('No scheme given for url: %s' % url)
-
- # make md5 hash of the url for caching...
- filename = '%s.xml' % hashlib.md5(url.encode('utf8')).hexdigest()
- if isinstance(cache, basestring):
- filename = os.path.join(cache, filename)
- if cache and os.path.exists(filename) and not force_download:
- log.info('Reading file %s' % filename)
- f = open(filename, 'r')
- xml = f.read()
- f.close()
- else:
- if url_scheme == 'file':
- log.info('Fetching url %s using urllib2' % url)
- f = urllib2.urlopen(url)
- xml = f.read()
- else:
- log.info('GET %s using %s' % (url, http._wrapper_version))
- response, xml = http.request(url, 'GET', None, {})
- if cache:
- log.info('Writing file %s' % filename)
- if not os.path.isdir(cache):
- os.makedirs(cache)
- f = open(filename, 'w')
- if isinstance(xml, bytes):
- xml = xml.decode('utf-8')
- f.write(xml)
- f.close()
- return xml
-
-
-def sort_dict(od, d):
- """Sort parameters (same order as xsd:sequence)"""
- if isinstance(od, dict):
- ret = OrderedDict()
- for k in od.keys():
- v = d.get(k)
- # don't append null tags!
- if v is not None:
- if isinstance(v, dict):
- v = sort_dict(od[k], v)
- elif isinstance(v, list):
- v = [sort_dict(od[k][0], v1) for v1 in v]
- ret[k] = v
- if hasattr(od, 'namespace'):
- ret.namespace = od.namespace
- return ret
- else:
- return d
-
-
-def make_key(element_name, element_type, namespace):
- """Return a suitable key for elements"""
- # only distinguish 'element' vs other types
- if element_type in ('complexType', 'simpleType'):
- eltype = 'complexType'
- else:
- eltype = element_type
- if eltype not in ('element', 'complexType', 'simpleType'):
- raise RuntimeError("Unknown element type %s = %s" % (element_name, eltype))
- return (element_name, eltype, namespace)
-
-
-def process_element(elements, element_name, node, element_type, xsd_uri, dialect, namespace,
- soapenc_uri = 'http://schemas.xmlsoap.org/soap/encoding/'):
- """Parse and define simple element types"""
-
-
-
- log.debug('Processing element %s %s' % (element_name, element_type))
- for tag in node:
- if tag.get_local_name() in ('annotation', 'documentation'):
- continue
- elif tag.get_local_name() in ('element', 'restriction'):
- log.debug('%s has no children! %s' % (element_name, tag))
- children = tag # element "alias"?
- alias = True
- elif tag.children():
- children = tag.children()
- alias = False
- else:
- log.debug('%s has no children! %s' % (element_name, tag))
- continue # TODO: abstract?
- d = OrderedDict()
- d.namespace = namespace
- for e in children:
- t = e['type']
- if not t:
- t = e['base'] # complexContent (extension)!
- if not t:
- t = 'anyType' # no type given!
- t = t.split(":")
- if len(t) > 1:
- ns, type_name = t
- else:
- ns, type_name = None, t[0]
- if element_name == type_name:
- pass # warning with infinite recursion
- uri = ns and e.get_namespace_uri(ns) or xsd_uri
- if uri in (xsd_uri, soapenc_uri) and type_name != 'Array':
- # look for the type, None == any
- fn = REVERSE_TYPE_MAP.get(type_name, None)
- elif uri == soapenc_uri and type_name == 'Array':
- # arrays of simple types (look at the attribute tags):
- fn = []
- for a in e.children():
- for k, v in a[:]:
- if k.endswith(":arrayType"):
- type_name = v
- if ":" in type_name:
- type_name = type_name[type_name.index(":")+1:]
- if "[]" in type_name:
- type_name = type_name[:type_name.index("[]")]
- fn.append(REVERSE_TYPE_MAP.get(type_name, None))
- else:
- fn = None
- if not fn:
- # simple / complex type, postprocess later
- if ns:
- fn_namespace = uri # use the specified namespace
- else:
- fn_namespace = namespace # use parent namespace (default)
- for k, v in e[:]:
- if k.startswith("xmlns:"):
- # get the namespace uri from the element
- fn_namespace = v
- fn = elements.setdefault(make_key(type_name, 'complexType', fn_namespace), OrderedDict())
-
- if e['maxOccurs'] == 'unbounded' or (uri == soapenc_uri and type_name == 'Array'):
- # it's an array... TODO: compound arrays? and check ns uri!
- if isinstance(fn, OrderedDict):
- if len(children) > 1 and dialect in ('jetty',):
- # Jetty style support
- # {'ClassName': [{'attr1': val1, 'attr2': val2}]
- fn.array = True
- else:
- # .NET style support (backward compatibility)
- # [{'ClassName': {'attr1': val1, 'attr2': val2}]
- d.array = True
- else:
- if dialect in ('jetty',):
- # scalar support [{'attr1': [val1]}]
- fn = [fn]
- else:
- d.array = True
-
- if e['name'] is not None and not alias:
- e_name = e['name']
- d[e_name] = fn
- else:
- log.debug('complexContent/simpleType/element %s = %s' % (element_name, type_name))
- d[None] = fn
- if e is not None and e.get_local_name() == 'extension' and e.children():
- # extend base element:
- process_element(elements, element_name, e.children(), element_type, xsd_uri, dialect, namespace)
- elements.setdefault(make_key(element_name, element_type, namespace), OrderedDict()).update(d)
-
-
-def postprocess_element(elements):
- """Fix unresolved references (elements referenced before its definition, thanks .net)"""
- for k, v in elements.items():
- if isinstance(v, OrderedDict):
- if v != elements: # TODO: fix recursive elements
- postprocess_element(v)
- if None in v and v[None]: # extension base?
- if isinstance(v[None], dict):
- for i, kk in enumerate(v[None]):
- # extend base -keep orginal order-
- if v[None] is not None:
- elements[k].insert(kk, v[None][kk], i)
- del v[None]
- else: # "alias", just replace
- log.debug('Replacing %s = %s' % (k, v[None]))
- elements[k] = v[None]
- #break
- if v.array:
- elements[k] = [v] # convert arrays to python lists
- if isinstance(v, list):
- for n in v: # recurse list
- if isinstance(n, (OrderedDict, list)):
- postprocess_element(n)
-
-
-def get_message(messages, message_name, part_name):
- if part_name:
- # get the specific part of the message:
- return messages.get((message_name, part_name))
- else:
- # get the first part for the specified message:
- for (message_name_key, part_name_key), message in messages.items():
- if message_name_key == message_name:
- return message
-
-
-get_local_name = lambda s: s and str((':' in s) and s.split(':')[1] or s)
-get_namespace_prefix = lambda s: s and str((':' in s) and s.split(':')[0] or None)
-
-
-def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces=None, qualified=False):
- """Find schema elements and complex types"""
-
- from .simplexml import SimpleXMLElement # here to avoid recursive imports
-
- # analyze the namespaces used in this schema
- local_namespaces = {}
- for k, v in schema[:]:
- if k.startswith("xmlns"):
- local_namespaces[get_local_name(k)] = v
- if k == 'targetNamespace':
- # URI namespace reference for this schema
- local_namespaces[None] = v
- if k == 'elementFormDefault' and v == "qualified" and qualified is None:
- qualified = True
- # add schema namespaces to the global namespace dict = {URI: ns prefix}
- for ns in local_namespaces.values():
- if ns not in global_namespaces:
- global_namespaces[ns] = 'ns%s' % len(global_namespaces)
-
- for element in schema.children() or []:
- if element.get_local_name() in ('import', 'include',):
- schema_namespace = element['namespace']
- schema_location = element['schemaLocation']
- if schema_location is None:
- log.debug('Schema location not provided for %s!' % schema_namespace)
- continue
- if schema_location in imported_schemas:
- log.debug('Schema %s already imported!' % schema_location)
- continue
- imported_schemas[schema_location] = schema_namespace
- log.debug('Importing schema %s from %s' % (schema_namespace, schema_location))
- # Open uri and read xml:
- xml = fetch(schema_location, http, cache, force_download, wsdl_basedir)
-
- # Parse imported XML schema (recursively):
- imported_schema = SimpleXMLElement(xml, namespace=xsd_uri)
- preprocess_schema(imported_schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces)
-
- element_type = element.get_local_name()
- if element_type in ('element', 'complexType', "simpleType"):
- namespace = local_namespaces[None] # get targetNamespace
- element_ns = global_namespaces[ns] # get the prefix
- element_name = element['name']
- log.debug("Parsing Element %s: %s" % (element_type, element_name))
- if element.get_local_name() == 'complexType':
- children = element.children()
- elif element.get_local_name() == 'simpleType':
- children = element('restriction', ns=xsd_uri)
- elif element.get_local_name() == 'element' and element['type']:
- children = element
- else:
- children = element.children()
- if children:
- children = children.children()
- elif element.get_local_name() == 'element':
- children = element
- if children:
- process_element(elements, element_name, children, element_type, xsd_uri, dialect, namespace)
-
-
-# simplexml utilities:
-
-try:
- _strptime = datetime.datetime.strptime
-except AttributeError: # python2.4
- _strptime = lambda s, fmt: datetime.datetime(*(time.strptime(s, fmt)[:6]))
-
-
-# Functions to serialize/deserialize special immutable types:
-def datetime_u(s):
- fmt = "%Y-%m-%dT%H:%M:%S"
- try:
- return _strptime(s, fmt)
- except ValueError:
- try:
- # strip utc offset
- if s[-3] == ":" and s[-6] in (' ', '-', '+'):
- warnings.warn('removing unsupported UTC offset', RuntimeWarning)
- s = s[:-6]
- # parse microseconds
- try:
- return _strptime(s, fmt + ".%f")
- except:
- return _strptime(s, fmt)
- except ValueError:
- # strip microseconds (not supported in this platform)
- if "." in s:
- warnings.warn('removing unsuppported microseconds', RuntimeWarning)
- s = s[:s.index(".")]
- return _strptime(s, fmt)
-
-datetime_m = lambda dt: dt.isoformat('T')
-date_u = lambda s: _strptime(s[0:10], "%Y-%m-%d").date()
-date_m = lambda d: d.strftime("%Y-%m-%d")
-time_u = lambda s: _strptime(s, "%H:%M:%S").time()
-time_m = lambda d: d.strftime("%H%M%S")
-bool_u = lambda s: {'0': False, 'false': False, '1': True, 'true': True}[s]
-bool_m = lambda s: {False: 'false', True: 'true'}[s]
-
-
-# aliases:
-class Alias(object):
- def __init__(self, py_type, xml_type):
- self.py_type, self.xml_type = py_type, xml_type
-
- def __call__(self, value):
- return self.py_type(value)
-
- def __repr__(self):
- return "" % (self.xml_type, self.py_type)
-
-if sys.version > '3':
- long = Alias(int, 'long')
-byte = Alias(str, 'byte')
-short = Alias(int, 'short')
-double = Alias(float, 'double')
-integer = Alias(long, 'integer')
-DateTime = datetime.datetime
-Date = datetime.date
-Time = datetime.time
-
-# Define convertion function (python type): xml schema type
-TYPE_MAP = {
- #str: 'string',
- unicode: 'string',
- bool: 'boolean',
- short: 'short',
- byte: 'byte',
- int: 'int',
- long: 'long',
- integer: 'integer',
- float: 'float',
- double: 'double',
- Decimal: 'decimal',
- datetime.datetime: 'dateTime',
- datetime.date: 'date',
-}
-TYPE_MARSHAL_FN = {
- datetime.datetime: datetime_m,
- datetime.date: date_m,
- bool: bool_m
-}
-TYPE_UNMARSHAL_FN = {
- datetime.datetime: datetime_u,
- datetime.date: date_u,
- bool: bool_u,
- str: unicode,
-}
-
-REVERSE_TYPE_MAP = dict([(v, k) for k, v in TYPE_MAP.items()])
-
-REVERSE_TYPE_MAP.update({
- 'base64Binary': str,
-})
-
-class OrderedDict(dict):
- """Minimal ordered dictionary for xsd:sequences"""
- def __init__(self):
- self.__keys = []
- self.array = False
- self.namespace = None
-
- def __setitem__(self, key, value):
- if key not in self.__keys:
- self.__keys.append(key)
- dict.__setitem__(self, key, value)
-
- def insert(self, key, value, index=0):
- if key not in self.__keys:
- self.__keys.insert(index, key)
- dict.__setitem__(self, key, value)
-
- def __delitem__(self, key):
- if key in self.__keys:
- self.__keys.remove(key)
- dict.__delitem__(self, key)
-
- def __iter__(self):
- return iter(self.__keys)
-
- def keys(self):
- return self.__keys
-
- def items(self):
- return [(key, self[key]) for key in self.__keys]
-
- def update(self, other):
- for k, v in other.items():
- self[k] = v
- # do not change if we are an array but the other is not:
- if isinstance(other, OrderedDict) and not self.array:
- self.array = other.array
- if isinstance(other, OrderedDict) and not self.namespace:
- self.namespace = other.namespace
-
- def copy(self):
- "Make a duplicate"
- new = OrderedDict()
- new.update(self)
- return new
-
- def __str__(self):
- return "%s" % dict.__str__(self)
-
- def __repr__(self):
- s = "{%s}" % ", ".join(['%s: %s' % (repr(k), repr(v)) for k, v in self.items()])
- if self.array and False:
- s = "[%s]" % s
- return s
-
diff --git a/pysimplesoap/server.py b/pysimplesoap/server.py
deleted file mode 100644
index 82fb33d..0000000
--- a/pysimplesoap/server.py
+++ /dev/null
@@ -1,560 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by the
-# Free Software Foundation; either version 3, 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 MERCHANTIBILITY
-# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
-# for more details.
-
-"""Pythonic simple SOAP Server implementation"""
-
-
-from __future__ import unicode_literals
-
-import sys
-import logging
-import re
-import traceback
-try:
- from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
-except ImportError:
- from http.server import BaseHTTPRequestHandler, HTTPServer
-
-from . import __author__, __copyright__, __license__, __version__
-from .simplexml import SimpleXMLElement, TYPE_MAP, Date, Decimal
-
-log = logging.getLogger(__name__)
-
-# Deprecated?
-NS_RX = re.compile(r'xmlns:(\w+)="(.+?)"')
-
-
-class SoapDispatcher(object):
- """Simple Dispatcher for SOAP Server"""
-
- def __init__(self, name, documentation='', action='', location='',
- namespace=None, prefix=False,
- soap_uri="http://schemas.xmlsoap.org/soap/envelope/",
- soap_ns='soap',
- namespaces={},
- pretty=False,
- debug=False,
- **kwargs):
- """
- :param namespace: Target namespace; xmlns=targetNamespace
- :param prefix: Prefix for target namespace; xmlns:prefix=targetNamespace
- :param namespaces: Specify additional namespaces; example: {'external': 'http://external.mt.moboperator'}
- :param pretty: Prettifies generated xmls
- :param debug: Use to add tracebacks in generated xmls.
-
- Multiple namespaces
- ===================
-
- It is possible to support multiple namespaces.
- You need to specify additional namespaces by passing `namespace` parameter.
-
- >>> dispatcher = SoapDispatcher(
- ... name = "MTClientWS",
- ... location = "http://localhost:8008/ws/MTClientWS",
- ... action = 'http://localhost:8008/ws/MTClientWS', # SOAPAction
- ... namespace = "http://external.mt.moboperator", prefix="external",
- ... documentation = 'moboperator MTClientWS',
- ... namespaces = {
- ... 'external': 'http://external.mt.moboperator',
- ... 'model': 'http://model.common.mt.moboperator'
- ... },
- ... ns = True)
-
- Now the registered method must return node names with namespaces' prefixes.
-
- >>> def _multi_ns_func(self, serviceMsisdn):
- ... ret = {
- ... 'external:activateSubscriptionsReturn': [
- ... {'model:code': '0'},
- ... {'model:description': 'desc'},
- ... ]}
- ... return ret
-
- Our prefixes will be changed to those used by the client.
- """
- self.methods = {}
- self.name = name
- self.documentation = documentation
- self.action = action # base SoapAction
- self.location = location
- self.namespace = namespace # targetNamespace
- self.prefix = prefix
- self.soap_ns = soap_ns
- self.soap_uri = soap_uri
- self.namespaces = namespaces
- self.pretty = pretty
- self.debug = debug
-
- @staticmethod
- def _extra_namespaces(xml, ns):
- """Extends xml with extra namespaces.
- :param ns: dict with namespaceUrl:prefix pairs
- :param xml: XML node to modify
- """
- if ns:
- _tpl = 'xmlns:%s="%s"'
- _ns_str = " ".join([_tpl % (prefix, uri) for uri, prefix in ns.items() if uri not in xml])
- xml = xml.replace('/>', ' ' + _ns_str + '/>')
- return xml
-
- def register_function(self, name, fn, returns=None, args=None, doc=None):
- self.methods[name] = fn, returns, args, doc or getattr(fn, "__doc__", "")
-
- def dispatch(self, xml, action=None):
- """Receive and process SOAP call"""
- # default values:
- prefix = self.prefix
- ret = fault = None
- soap_ns, soap_uri = self.soap_ns, self.soap_uri
- soap_fault_code = 'VersionMismatch'
- name = None
-
- # namespaces = [('model', 'http://model.common.mt.moboperator'), ('external', 'http://external.mt.moboperator')]
- _ns_reversed = dict(((v, k) for k, v in self.namespaces.items())) # Switch keys-values
- # _ns_reversed = {'http://external.mt.moboperator': 'external', 'http://model.common.mt.moboperator': 'model'}
-
- try:
- request = SimpleXMLElement(xml, namespace=self.namespace)
-
- # detect soap prefix and uri (xmlns attributes of Envelope)
- for k, v in request[:]:
- if v in ("http://schemas.xmlsoap.org/soap/envelope/",
- "http://www.w3.org/2003/05/soap-env",):
- soap_ns = request.attributes()[k].localName
- soap_uri = request.attributes()[k].value
-
- # If the value from attributes on Envelope is in additional namespaces
- elif v in self.namespaces.values():
- _ns = request.attributes()[k].localName
- _uri = request.attributes()[k].value
- _ns_reversed[_uri] = _ns # update with received alias
- # Now we change 'external' and 'model' to the received forms i.e. 'ext' and 'mod'
- # After that we know how the client has prefixed additional namespaces
-
- ns = NS_RX.findall(xml)
- for k, v in ns:
- if v in self.namespaces.values():
- _ns_reversed[v] = k
-
- soap_fault_code = 'Client'
-
- # parse request message and get local method
- method = request('Body', ns=soap_uri).children()(0)
- if action:
- # method name = action
- name = action[len(self.action)+1:-1]
- prefix = self.prefix
- if not action or not name:
- # method name = input message name
- name = method.get_local_name()
- prefix = method.get_prefix()
-
- log.debug('dispatch method: %s', name)
- function, returns_types, args_types, doc = self.methods[name]
- log.debug('returns_types %s', returns_types)
-
- # de-serialize parameters (if type definitions given)
- if args_types:
- args = method.children().unmarshall(args_types)
- elif args_types is None:
- args = {'request': method} # send raw request
- else:
- args = {} # no parameters
-
- soap_fault_code = 'Server'
- # execute function
- ret = function(**args)
- log.debug('dispathed method returns: %s', ret)
-
- except Exception: # This shouldn't be one huge try/except
- import sys
- etype, evalue, etb = sys.exc_info()
- log.error(traceback.format_exc())
- if self.debug:
- detail = ''.join(traceback.format_exception(etype, evalue, etb))
- detail += '\n\nXML REQUEST\n\n' + xml
- else:
- detail = None
- fault = {'faultcode': "%s.%s" % (soap_fault_code, etype.__name__),
- 'faultstring': evalue,
- 'detail': detail}
-
- # build response message
- if not prefix:
- xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s"/>"""
- else:
- xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s"
- xmlns:%(prefix)s="%(namespace)s"/>"""
-
- xml %= { # a %= {} is a shortcut for a = a % {}
- 'namespace': self.namespace,
- 'prefix': prefix,
- 'soap_ns': soap_ns,
- 'soap_uri': soap_uri
- }
-
- # Now we add extra namespaces
- xml = SoapDispatcher._extra_namespaces(xml, _ns_reversed)
-
- # Change our namespace alias to that given by the client.
- # We put [('model', 'http://model.common.mt.moboperator'), ('external', 'http://external.mt.moboperator')]
- # mix it with {'http://external.mt.moboperator': 'ext', 'http://model.common.mt.moboperator': 'mod'}
- mapping = dict(((k, _ns_reversed[v]) for k, v in self.namespaces.items())) # Switch keys-values and change value
- # and get {'model': u'mod', 'external': u'ext'}
-
- response = SimpleXMLElement(xml,
- namespace=self.namespace,
- namespaces_map=mapping,
- prefix=prefix)
-
- response['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
- response['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema"
-
- body = response.add_child("%s:Body" % soap_ns, ns=False)
-
- if fault:
- # generate a Soap Fault (with the python exception)
- body.marshall("%s:Fault" % soap_ns, fault, ns=False)
- else:
- # return normal value
- res = body.add_child("%sResponse" % name, ns=prefix)
- if not prefix:
- res['xmlns'] = self.namespace # add target namespace
-
- # serialize returned values (response) if type definition available
- if returns_types:
- if not isinstance(ret, dict):
- res.marshall(returns_types.keys()[0], ret, )
- else:
- for k, v in ret.items():
- res.marshall(k, v)
- elif returns_types is None:
- # merge xmlelement returned
- res.import_node(ret)
- elif returns_types == {}:
- log.warning('Given returns_types is an empty dict.')
-
- return response.as_xml(pretty=self.pretty)
-
- # Introspection functions:
-
- def list_methods(self):
- """Return a list of aregistered operations"""
- return [(method, doc) for method, (function, returns, args, doc) in self.methods.items()]
-
- def help(self, method=None):
- """Generate sample request and response messages"""
- (function, returns, args, doc) = self.methods[method]
- xml = """
-
-<%(method)s xmlns="%(namespace)s"/>
-""" % {'method': method, 'namespace': self.namespace}
- request = SimpleXMLElement(xml, namespace=self.namespace, prefix=self.prefix)
- if args:
- items = args.items()
- elif args is None:
- items = [('value', None)]
- else:
- items = []
- for k, v in items:
- request(method).marshall(k, v, add_comments=True, ns=False)
-
- xml = """
-
-<%(method)sResponse xmlns="%(namespace)s"/>
-""" % {'method': method, 'namespace': self.namespace}
- response = SimpleXMLElement(xml, namespace=self.namespace, prefix=self.prefix)
- if returns:
- items = returns.items()
- elif args is None:
- items = [('value', None)]
- else:
- items = []
- for k, v in items:
- response('%sResponse' % method).marshall(k, v, add_comments=True, ns=False)
-
- return request.as_xml(pretty=True), response.as_xml(pretty=True), doc
-
- def wsdl(self):
- """Generate Web Service Description v1.1"""
- xml = """
-
- %(documentation)s
-
-
-
-
-
-
-
-""" % {'namespace': self.namespace, 'name': self.name, 'documentation': self.documentation}
- wsdl = SimpleXMLElement(xml)
-
- for method, (function, returns, args, doc) in self.methods.items():
- # create elements:
-
- def parse_element(name, values, array=False, complex=False):
- if not complex:
- element = wsdl('wsdl:types')('xsd:schema').add_child('xsd:element')
- complex = element.add_child("xsd:complexType")
- else:
- complex = wsdl('wsdl:types')('xsd:schema').add_child('xsd:complexType')
- element = complex
- element['name'] = name
- if values:
- items = values
- elif values is None:
- items = [('value', None)]
- else:
- items = []
- if not array and items:
- all = complex.add_child("xsd:all")
- elif items:
- all = complex.add_child("xsd:sequence")
- for k, v in items:
- e = all.add_child("xsd:element")
- e['name'] = k
- if array:
- e[:] = {'minOccurs': "0", 'maxOccurs': "unbounded"}
- if v in TYPE_MAP.keys():
- t = 'xsd:%s' % TYPE_MAP[v]
- elif v is None:
- t = 'xsd:anyType'
- elif isinstance(v, list):
- n = "ArrayOf%s%s" % (name, k)
- l = []
- for d in v:
- l.extend(d.items())
- parse_element(n, l, array=True, complex=True)
- t = "tns:%s" % n
- elif isinstance(v, dict):
- n = "%s%s" % (name, k)
- parse_element(n, v.items(), complex=True)
- t = "tns:%s" % n
- e.add_attribute('type', t)
-
- parse_element("%s" % method, args and args.items())
- parse_element("%sResponse" % method, returns and returns.items())
-
- # create messages:
- for m, e in ('Input', ''), ('Output', 'Response'):
- message = wsdl.add_child('wsdl:message')
- message['name'] = "%s%s" % (method, m)
- part = message.add_child("wsdl:part")
- part[:] = {'name': 'parameters',
- 'element': 'tns:%s%s' % (method, e)}
-
- # create ports
- portType = wsdl.add_child('wsdl:portType')
- portType['name'] = "%sPortType" % self.name
- for method, (function, returns, args, doc) in self.methods.items():
- op = portType.add_child('wsdl:operation')
- op['name'] = method
- if doc:
- op.add_child("wsdl:documentation", doc)
- input = op.add_child("wsdl:input")
- input['message'] = "tns:%sInput" % method
- output = op.add_child("wsdl:output")
- output['message'] = "tns:%sOutput" % method
-
- # create bindings
- binding = wsdl.add_child('wsdl:binding')
- binding['name'] = "%sBinding" % self.name
- binding['type'] = "tns:%sPortType" % self.name
- soapbinding = binding.add_child('soap:binding')
- soapbinding['style'] = "document"
- soapbinding['transport'] = "http://schemas.xmlsoap.org/soap/http"
- for method in self.methods.keys():
- op = binding.add_child('wsdl:operation')
- op['name'] = method
- soapop = op.add_child('soap:operation')
- soapop['soapAction'] = self.action + method
- soapop['style'] = 'document'
- input = op.add_child("wsdl:input")
- ##input.add_attribute('name', "%sInput" % method)
- soapbody = input.add_child("soap:body")
- soapbody["use"] = "literal"
- output = op.add_child("wsdl:output")
- ##output.add_attribute('name', "%sOutput" % method)
- soapbody = output.add_child("soap:body")
- soapbody["use"] = "literal"
-
- service = wsdl.add_child('wsdl:service')
- service["name"] = "%sService" % self.name
- service.add_child('wsdl:documentation', text=self.documentation)
- port = service.add_child('wsdl:port')
- port["name"] = "%s" % self.name
- port["binding"] = "tns:%sBinding" % self.name
- soapaddress = port.add_child('soap:address')
- soapaddress["location"] = self.location
- return wsdl.as_xml(pretty=True)
-
-
-class SOAPHandler(BaseHTTPRequestHandler):
-
- def do_GET(self):
- """User viewable help information and wsdl"""
- args = self.path[1:].split("?")
- if self.path != "/" and args[0] not in self.server.dispatcher.methods.keys():
- self.send_error(404, "Method not found: %s" % args[0])
- else:
- if self.path == "/":
- # return wsdl if no method supplied
- response = self.server.dispatcher.wsdl()
- else:
- # return supplied method help (?request or ?response messages)
- req, res, doc = self.server.dispatcher.help(args[0])
- if len(args) == 1 or args[1] == "request":
- response = req
- else:
- response = res
- self.send_response(200)
- self.send_header("Content-type", "text/xml")
- self.end_headers()
- self.wfile.write(response)
-
- def do_POST(self):
- """SOAP POST gateway"""
- self.send_response(200)
- self.send_header("Content-type", "text/xml")
- self.end_headers()
- request = self.rfile.read(int(self.headers.getheader('content-length')))
- response = self.server.dispatcher.dispatch(request)
- self.wfile.write(response)
-
-
-class WSGISOAPHandler(object):
-
- def __init__(self, dispatcher):
- self.dispatcher = dispatcher
-
- def __call__(self, environ, start_response):
- return self.handler(environ, start_response)
-
- def handler(self, environ, start_response):
- if environ['REQUEST_METHOD'] == 'GET':
- return self.do_get(environ, start_response)
- elif environ['REQUEST_METHOD'] == 'POST':
- return self.do_post(environ, start_response)
- else:
- start_response('405 Method not allowed', [('Content-Type', 'text/plain')])
- return ['Method not allowed']
-
- def do_get(self, environ, start_response):
- path = environ.get('PATH_INFO').lstrip('/')
- query = environ.get('QUERY_STRING')
- if path != "" and path not in self.dispatcher.methods.keys():
- start_response('404 Not Found', [('Content-Type', 'text/plain')])
- return ["Method not found: %s" % path]
- elif path == "":
- # return wsdl if no method supplied
- response = self.dispatcher.wsdl()
- else:
- # return supplied method help (?request or ?response messages)
- req, res, doc = self.dispatcher.help(path)
- if len(query) == 0 or query == "request":
- response = req
- else:
- response = res
- start_response('200 OK', [('Content-Type', 'text/xml'), ('Content-Length', str(len(response)))])
- return [response]
-
- def do_post(self, environ, start_response):
- length = int(environ['CONTENT_LENGTH'])
- request = environ['wsgi.input'].read(length)
- response = self.dispatcher.dispatch(request)
- start_response('200 OK', [('Content-Type', 'text/xml'), ('Content-Length', str(len(response)))])
- return [response]
-
-
-if __name__ == "__main__":
-
- dispatcher = SoapDispatcher(
- name="PySimpleSoapSample",
- location="http://localhost:8008/",
- action='http://localhost:8008/', # SOAPAction
- namespace="http://example.com/pysimplesoapsamle/", prefix="ns0",
- documentation='Example soap service using PySimpleSoap',
- trace=True,
- ns=True)
-
- def adder(p, c, dt=None):
- """Add several values"""
- import datetime
- dt = dt + datetime.timedelta(365)
- return {'ab': p['a'] + p['b'], 'dd': c[0]['d'] + c[1]['d'], 'dt': dt}
-
- def dummy(in0):
- """Just return input"""
- return in0
-
- def echo(request):
- """Copy request->response (generic, any type)"""
- return request.value
-
- dispatcher.register_function(
- 'Adder', adder,
- returns={'AddResult': {'ab': int, 'dd': str}},
- args={'p': {'a': int, 'b': int}, 'dt': Date, 'c': [{'d': Decimal}]}
- )
-
- dispatcher.register_function(
- 'Dummy', dummy,
- returns={'out0': str},
- args={'in0': str}
- )
-
- dispatcher.register_function('Echo', echo)
-
- if '--local' in sys.argv:
-
- wsdl = dispatcher.wsdl()
-
- for method, doc in dispatcher.list_methods():
- request, response, doc = dispatcher.help(method)
-
- if '--serve' in sys.argv:
- log.info("Starting server...")
- httpd = HTTPServer(("", 8008), SOAPHandler)
- httpd.dispatcher = dispatcher
- httpd.serve_forever()
-
- if '--wsgi-serve' in sys.argv:
- log.info("Starting wsgi server...")
- from wsgiref.simple_server import make_server
- application = WSGISOAPHandler(dispatcher)
- wsgid = make_server('', 8008, application)
- wsgid.serve_forever()
-
- if '--consume' in sys.argv:
- from .client import SoapClient
- client = SoapClient(
- location="http://localhost:8008/",
- action='http://localhost:8008/', # SOAPAction
- namespace="http://example.com/sample.wsdl",
- soap_ns='soap',
- trace=True,
- ns=False
- )
- p = {'a': 1, 'b': 2}
- c = [{'d': '1.20'}, {'d': '2.01'}]
- response = client.Adder(p=p, dt='20100724', c=c)
- result = response.AddResult
- log.info(int(result.ab))
- log.info(str(result.dd))
diff --git a/pysimplesoap/simplexml.py b/pysimplesoap/simplexml.py
deleted file mode 100644
index 61538e2..0000000
--- a/pysimplesoap/simplexml.py
+++ /dev/null
@@ -1,481 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by the
-# Free Software Foundation; either version 3, 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 MERCHANTIBILITY
-# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
-# for more details.
-
-"""Simple XML manipulation"""
-
-
-from __future__ import unicode_literals
-import sys
-if sys.version > '3':
- basestring = str
- unicode = str
-
-import logging
-import re
-import time
-import xml.dom.minidom
-
-from . import __author__, __copyright__, __license__, __version__
-
-# Utility functions used for marshalling, moved aside for readability
-from .helpers import TYPE_MAP, TYPE_MARSHAL_FN, TYPE_UNMARSHAL_FN, \
- REVERSE_TYPE_MAP, OrderedDict, Date, Decimal
-
-log = logging.getLogger(__name__)
-
-
-class SimpleXMLElement(object):
- """Simple XML manipulation (simil PHP)"""
-
- def __init__(self, text=None, elements=None, document=None,
- namespace=None, prefix=None, namespaces_map={}, jetty=False):
- """
- :param namespaces_map: How to map our namespace prefix to that given by the client;
- {prefix: received_prefix}
- """
- self.__namespaces_map = namespaces_map
- _rx = "|".join(namespaces_map.keys()) # {'external': 'ext', 'model': 'mod'} -> 'external|model'
- self.__ns_rx = re.compile(r"^(%s):.*$" % _rx) # And now we build an expression ^(external|model):.*$
- # to find prefixes in all xml nodes i.e.: 1
- # and later change that to 1
- self.__ns = namespace
- self.__prefix = prefix
- self.__jetty = jetty # special list support
-
- if text is not None:
- try:
- self.__document = xml.dom.minidom.parseString(text)
- except:
- log.error(text)
- raise
- self.__elements = [self.__document.documentElement]
- else:
- self.__elements = elements
- self.__document = document
-
- def add_child(self, name, text=None, ns=True):
- """Adding a child tag to a node"""
- if not ns or self.__ns is False:
- ##log.debug('adding %s without namespace', name)
- element = self.__document.createElement(name)
- else:
- ##log.debug('adding %s ns "%s" %s', name, self.__ns, ns)
- if isinstance(ns, basestring):
- element = self.__document.createElement(name)
- element.setAttribute("xmlns", ns)
- elif self.__prefix:
- element = self.__document.createElementNS(self.__ns, "%s:%s" % (self.__prefix, name))
- else:
- element = self.__document.createElementNS(self.__ns, name)
- # don't append null tags!
- if text is not None:
- element.appendChild(self.__document.createTextNode(text))
- self._element.appendChild(element)
- return SimpleXMLElement(
- elements=[element],
- document=self.__document,
- namespace=self.__ns,
- prefix=self.__prefix,
- jetty=self.__jetty,
- namespaces_map=self.__namespaces_map
- )
-
- def __setattr__(self, tag, text):
- """Add text child tag node (short form)"""
- if tag.startswith("_"):
- object.__setattr__(self, tag, text)
- else:
- ##log.debug('__setattr__(%s, %s)', tag, text)
- self.add_child(tag, text)
-
- def __delattr__(self, tag):
- """Remove a child tag (non recursive!)"""
- elements = [__element for __element in self._element.childNodes
- if __element.nodeType == __element.ELEMENT_NODE]
- for element in elements:
- self._element.removeChild(element)
-
- def add_comment(self, data):
- """Add an xml comment to this child"""
- comment = self.__document.createComment(data)
- self._element.appendChild(comment)
-
- def as_xml(self, filename=None, pretty=False):
- """Return the XML representation of the document"""
- if not pretty:
- return self.__document.toxml('UTF-8')
- else:
- return self.__document.toprettyxml(encoding='UTF-8')
-
- def __repr__(self):
- """Return the XML representation of this tag"""
- # NOTE: do not use self.as_xml('UTF-8') as it returns the whole xml doc
- return self._element.toxml('UTF-8')
-
- def get_name(self):
- """Return the tag name of this node"""
- return self._element.tagName
-
- def get_local_name(self):
- """Return the tag local name (prefix:name) of this node"""
- return self._element.localName
-
- def get_prefix(self):
- """Return the namespace prefix of this node"""
- return self._element.prefix
-
- def get_namespace_uri(self, ns):
- """Return the namespace uri for a prefix"""
- element = self._element
- while element is not None and element.attributes is not None:
- try:
- return element.attributes['xmlns:%s' % ns].value
- except KeyError:
- element = element.parentNode
-
- def attributes(self):
- """Return a dict of attributes for this tag"""
- #TODO: use slice syntax [:]?
- return self._element.attributes
-
- def __getitem__(self, item):
- """Return xml tag attribute value or a slice of attributes (iter)"""
- ##log.debug('__getitem__(%s)', item)
- if isinstance(item, basestring):
- if self._element.hasAttribute(item):
- return self._element.attributes[item].value
- elif isinstance(item, slice):
- # return a list with name:values
- return list(self._element.attributes.items())[item]
- else:
- # return element by index (position)
- element = self.__elements[item]
- return SimpleXMLElement(
- elements=[element],
- document=self.__document,
- namespace=self.__ns,
- prefix=self.__prefix,
- jetty=self.__jetty,
- namespaces_map=self.__namespaces_map
- )
-
- def add_attribute(self, name, value):
- """Set an attribute value from a string"""
- self._element.setAttribute(name, value)
-
- def __setitem__(self, item, value):
- """Set an attribute value"""
- if isinstance(item, basestring):
- self.add_attribute(item, value)
- elif isinstance(item, slice):
- # set multiple attributes at once
- for k, v in value.items():
- self.add_attribute(k, v)
-
- def __call__(self, tag=None, ns=None, children=False, root=False,
- error=True, ):
- """Search (even in child nodes) and return a child tag by name"""
- try:
- if root:
- # return entire document
- return SimpleXMLElement(
- elements=[self.__document.documentElement],
- document=self.__document,
- namespace=self.__ns,
- prefix=self.__prefix,
- jetty=self.__jetty,
- namespaces_map=self.__namespaces_map
- )
- if tag is None:
- # if no name given, iterate over siblings (same level)
- return self.__iter__()
- if children:
- # future: filter children? by ns?
- return self.children()
- elements = None
- if isinstance(tag, int):
- # return tag by index
- elements = [self.__elements[tag]]
- if ns and not elements:
- for ns_uri in isinstance(ns, (tuple, list)) and ns or (ns, ):
- ##log.debug('searching %s by ns=%s', tag, ns_uri)
- elements = self._element.getElementsByTagNameNS(ns_uri, tag)
- if elements:
- break
- if self.__ns and not elements:
- ##log.debug('searching %s by ns=%s', tag, self.__ns)
- elements = self._element.getElementsByTagNameNS(self.__ns, tag)
- if not elements:
- ##log.debug('searching %s', tag)
- elements = self._element.getElementsByTagName(tag)
- if not elements:
- ##log.debug(self._element.toxml())
- if error:
- raise AttributeError("No elements found")
- else:
- return
- return SimpleXMLElement(
- elements=elements,
- document=self.__document,
- namespace=self.__ns,
- prefix=self.__prefix,
- jetty=self.__jetty,
- namespaces_map=self.__namespaces_map)
- except AttributeError as e:
- raise AttributeError("Tag not found: %s (%s)" % (tag, e))
-
- def __getattr__(self, tag):
- """Shortcut for __call__"""
- return self.__call__(tag)
-
- def __iter__(self):
- """Iterate over xml tags at this level"""
- try:
- for __element in self.__elements:
- yield SimpleXMLElement(
- elements=[__element],
- document=self.__document,
- namespace=self.__ns,
- prefix=self.__prefix,
- jetty=self.__jetty,
- namespaces_map=self.__namespaces_map)
- except:
- raise
-
- def __dir__(self):
- """List xml children tags names"""
- return [node.tagName for node
- in self._element.childNodes
- if node.nodeType != node.TEXT_NODE]
-
- def children(self):
- """Return xml children tags element"""
- elements = [__element for __element in self._element.childNodes
- if __element.nodeType == __element.ELEMENT_NODE]
- if not elements:
- return None
- #raise IndexError("Tag %s has no children" % self._element.tagName)
- return SimpleXMLElement(
- elements=elements,
- document=self.__document,
- namespace=self.__ns,
- prefix=self.__prefix,
- jetty=self.__jetty,
- namespaces_map=self.__namespaces_map
- )
-
- def __len__(self):
- """Return element count"""
- return len(self.__elements)
-
- def __contains__(self, item):
- """Search for a tag name in this element or child nodes"""
- return self._element.getElementsByTagName(item)
-
- def __unicode__(self):
- """Returns the unicode text nodes of the current element"""
- if self._element.childNodes:
- rc = ""
- for node in self._element.childNodes:
- if node.nodeType == node.TEXT_NODE:
- rc = rc + node.data
- return rc
- return ''
-
- def __str__(self):
- """Returns the str text nodes of the current element"""
- return self.__unicode__()
-
- def __int__(self):
- """Returns the integer value of the current element"""
- return int(self.__str__())
-
- def __float__(self):
- """Returns the float value of the current element"""
- try:
- return float(self.__str__())
- except:
- raise IndexError(self._element.toxml())
-
- _element = property(lambda self: self.__elements[0])
-
- def unmarshall(self, types, strict=True):
- #import pdb; pdb.set_trace()
-
- """Convert to python values the current serialized xml element"""
- # types is a dict of {tag name: convertion function}
- # strict=False to use default type conversion if not specified
- # example: types={'p': {'a': int,'b': int}, 'c': [{'d':str}]}
- # expected xml: