Compatibilidad con Python 3.13

- Actualiza dependencias: cryptography>=41.0.0, pyOpenSSL>=23.2.0,
  xmlsec>=1.3.12, xmlsig>=0.1.9, zeep>=4.2.1, lxml>=5.2.2
- Anade monkey-patch para xades.load_pkcs12 con pyOpenSSL >= 24
  (PKCS12 eliminado de OpenSSL.crypto)
- Reemplaza datetime.utcnow() por datetime.now(timezone.utc)
- Reemplaza APIs deprecadas de lxml en facho.py y codelist
- Migra verificacion PKCS12 de pyOpenSSL a cryptography API en fe.py
- Moderniza typing: Union/Optional/List a sintaxis | y list[]
- Actualiza requirements_dev.txt con versiones recientes
- Actualiza tox.ini y Makefile.dev para Python 3.13
This commit is contained in:
2026-07-02 22:45:33 -05:00
parent f98ab98c9c
commit 93a3d18e32
13 changed files with 89 additions and 74 deletions

View File

@@ -7,34 +7,40 @@ RUN apt install software-properties-common -y \
&& add-apt-repository ppa:deadsnakes/ppa
RUN apt-get install -y --no-install-recommends \
python3.9 python3.9-distutils python3.9-dev \
python3.10 python3.10-distutils python3.10-dev \
python3.11 python3.11-distutils python3.11-dev \
python3.12 python3-setuptools python3.12-dev \
python3.13 python3-setuptools python3.13-dev \
wget \
ca-certificates
RUN wget https://bootstrap.pypa.io/get-pip.py \
&& python3.9 get-pip.py pip==23.2.1 --break-system-packages \
&& python3.10 get-pip.py pip==23.2.1 --break-system-packages \
&& python3.11 get-pip.py pip==23.2.1 --break-system-packages \
&& python3.12 get-pip.py pip==23.2.1 --break-system-packages \
&& python3.13 get-pip.py pip==23.2.1 --break-system-packages \
&& rm get-pip.py
RUN apt-get install -y --no-install-recommends \
libxml2-dev \
libxmlsec1-dev \
build-essential \
gcc \
build-essential \
pkg-config \
libxml2-dev \
libxmlsec1-dev \
xmlsec1 \
libxml2-dev \
libxslt-dev \
zip
RUN python3.9 --version
RUN python3.10 --version
RUN python3.11 --version
RUN python3.12 --version
RUN python3.13 --version
RUN pip3.9 install setuptools setuptools-rust
RUN pip3.10 install setuptools setuptools-rust
RUN pip3.11 install setuptools setuptools-rust --break-system-packages
RUN pip3.12 install setuptools setuptools-rust --break-system-packages
RUN pip3.13 install setuptools setuptools-rust --break-system-packages
RUN pip3 install tox pytest --break-system-packages

View File

@@ -15,7 +15,7 @@ dev-shell:
docker run --rm -ti -v "$(PWD):/app" -w /app --name facho-cli facho bash
test:
docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.12 setup.py test'
docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.13 -m pytest'
tox:
docker run -it -v $(PWD)/:/app -w /app facho tox

View File

@@ -140,12 +140,12 @@ class LXMLBuilder:
attrs['pretty_print'] = attrs.pop('pretty_print', False)
attrs['encoding'] = attrs.pop('encoding', 'UTF-8')
for el in elem.getiterator():
for el in elem.iter():
keys = filter(lambda key: key.startswith('facho_'), el.keys())
self.remove_attributes(el, keys, exclude=['facho_optional'])
is_optional = el.get('facho_optional', 'False') == 'True'
if is_optional and el.getchildren() == [] and el.keys() == [
if is_optional and list(el) == [] and el.keys() == [
'facho_optional']:
el.getparent().remove(el)
@@ -291,7 +291,7 @@ class FachoXML:
# se fuerza la adicion como un nuevo elemento
if append:
last_slibing = None
for child in parent.getchildren():
for child in list(parent):
if child.tag == node_tag:
last_slibing = child

View File

@@ -8,7 +8,7 @@ import xmlsec
import urllib.request
from datetime import datetime
from dataclasses import dataclass, asdict, field
from typing import List
import http.client
import hashlib
import secrets
@@ -47,7 +47,7 @@ class GetNumberingRangeResponse:
ValidateDateTo: str
TechnicalKey: str
NumberRangeResponse: List[NumberRangeResponse]
NumberRangeResponse: list[NumberRangeResponse]
@classmethod
@@ -91,7 +91,7 @@ class SendBillAsync(SOAPService):
@dataclass
class SendTestSetAsyncResponse:
ZipKey: str
ErrorMessageList: List[str]
ErrorMessageList: list[str]
@classmethod
def fromdict(cls, data):
@@ -134,7 +134,7 @@ class GetStatusResponse:
IsValid: bool
StatusDescription: str
StatusCode: int
ErrorMessage: List[str]
ErrorMessage: list[str]
@classmethod
def fromdict(cls, data):

View File

@@ -9,7 +9,7 @@ module.
"""
import pytz
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from lxml import etree
from lxml.etree import QName
@@ -222,7 +222,7 @@ def sign_envelope(
return _sign_envelope_with_key(envelope, key, signature_method, digest_method)
def get_timestamp(timestamp = None, delta=None):
timestamp = timestamp or datetime.utcnow()
timestamp = timestamp or datetime.now(timezone.utc)
if delta:
timestamp += delta

View File

@@ -35,8 +35,8 @@ class CodeList:
row = {}
#construir registro...
for value in xmlrow.getchildren():
row[value.attrib['ColumnRef']] = value.getchildren()[0].text
for value in list(xmlrow):
row[value.attrib['ColumnRef']] = list(value)[0].text
return row

View File

@@ -5,7 +5,6 @@ import uuid
import xmlsig
import xades
from datetime import datetime
import OpenSSL
import zipfile
# import warnings
import hashlib
@@ -18,6 +17,21 @@ from dateutil import tz
from cryptography.hazmat.primitives.serialization import pkcs12
# Monkey-patch xades/xmlsig para compatibilidad con pyOpenSSL >= 24
# (PKCS12 fue eliminado de OpenSSL.crypto, pero ambas librerías lo referencian)
import OpenSSL
import xades.xades_context
if not hasattr(OpenSSL.crypto, 'PKCS12'):
def _patched_load_pkcs12(self, key):
if isinstance(key, tuple):
self.x509 = key[1]
self.public_key = key[1].public_key()
self.private_key = key[0]
else:
raise NotImplementedError("unsupported key type")
xades.xades_context.XAdESContext.load_pkcs12 = _patched_load_pkcs12
AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code']
AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code']
@@ -444,9 +458,6 @@ class DianXMLExtensionSigner:
self._pkcs12_data,
self._passphrase))
# ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(
# self._pkcs12_data,
# self._passphrase))
if self._localpolicy:
with mock_xades_policy():
ctx.sign(signature)
@@ -590,8 +601,8 @@ class DianXMLExtensionSignerVerifier:
if isinstance(self._pkcs12_path_or_bytes, str):
pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read()
ctx = xades.XAdESContext()
ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(pkcs12_data,
self._passphrase))
ctx.load_pkcs12(pkcs12.load_key_and_certificates(
pkcs12_data, self._passphrase))
try:
if self._localpolicy:
with mock_xades_policy():

View File

@@ -11,7 +11,6 @@ from datetime import datetime, date
# from collections import defaultdict
import decimal
from decimal import Decimal
import typing
from ..data.dian import codelist
DECIMAL_PRECISION = 6
@@ -60,7 +59,7 @@ class AmountCollection(Collection):
class Amount:
def __init__(
self, amount: typing.Union[int, float, str, "Amount"],
self, amount: int | float | str | "Amount",
currency: Currency = Currency('COP')):
# DIAN 1.7.-2020: 1.2.3.1
@@ -294,7 +293,7 @@ class TaxScheme:
class Party:
name: str
ident: str
responsability_code: typing.List[Responsability]
responsability_code: list[Responsability]
responsability_regime_code: str
organization_code: str
tax_scheme: TaxScheme = field(default_factory=lambda: TaxScheme('01'))
@@ -327,7 +326,7 @@ class TaxScheme:
@dataclass
class TaxSubTotal:
percent: float
scheme: typing.Optional[TaxScheme] = None
scheme: TaxScheme | None = None
tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline):
@@ -359,7 +358,7 @@ class TaxTotalOmit(TaxTotal):
@dataclass
class WithholdingTaxSubTotal:
percent: float
scheme: typing.Optional[TaxScheme] = None
scheme: TaxScheme | None = None
tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline):
@@ -491,7 +490,7 @@ class AllowanceCharge:
reason: AllowanceChargeReason = None
# Valor Base para calcular el descuento o el cargo
base_amount: typing.Optional[Amount] = field(
base_amount: Amount | None = field(
default_factory=lambda: Amount(0.0))
# Porcentaje: Porcentaje que aplicar.
@@ -536,9 +535,9 @@ class InvoiceLine:
# ya que al reportar los totales es sobre
# la factura y el percent es unico por type_code
# de subtotal
tax: typing.Optional[TaxTotal]
withholding: typing.Optional[WithholdingTaxTotal]
allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(
tax: TaxTotal | None
withholding: WithholdingTaxTotal | None
allowance_charge: list[AllowanceCharge] = dataclasses.field(
default_factory=list)
def add_allowance_charge(self, charge):

View File

@@ -8,7 +8,7 @@
from dataclasses import dataclass
from datetime import datetime
import hashlib
import typing
from .. import fe
from .. import form
@@ -86,13 +86,13 @@ class NumeroSecuencia:
@dataclass
class Periodo:
fecha_ingreso: typing.Union[str, Fecha]
fecha_liquidacion_inicio: typing.Union[str, Fecha]
fecha_liquidacion_fin: typing.Union[str, Fecha]
fecha_generacion: typing.Union[str, Fecha]
fecha_ingreso: str | Fecha
fecha_liquidacion_inicio: str | Fecha
fecha_liquidacion_fin: str | Fecha
fecha_generacion: str | Fecha
tiempo_laborado: int = 1
fecha_retiro: typing.Union[str, Fecha] = None
fecha_retiro: str | Fecha | None = None
def __post_init__(self):
self.fecha_ingreso = Fecha.cast(self.fecha_ingreso)
@@ -259,7 +259,7 @@ class InformacionGeneral:
def __str__(self):
self.valor
fecha_generacion: typing.Union[str, Fecha]
fecha_generacion: str | Fecha
hora_generacion: str
periodo_nomina: PeriodoNomina
tipo_moneda: TipoMoneda

View File

@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import List
from ..amount import Amount
from .devengado import Devengado
@@ -30,7 +30,7 @@ class DevengadoHoraExtra:
@dataclass
class DevengadoHorasExtrasDiarias(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HEDs')
@@ -39,7 +39,7 @@ class DevengadoHorasExtrasDiarias(Devengado):
@dataclass
class DevengadoHorasExtrasNocturnas(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HENs')
@@ -49,7 +49,7 @@ class DevengadoHorasExtrasNocturnas(Devengado):
@dataclass
class DevengadoHorasRecargoNocturno(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HRNs')
@@ -58,7 +58,7 @@ class DevengadoHorasRecargoNocturno(Devengado):
@dataclass
class DevengadoHorasExtrasDiariasDominicalesYFestivos(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HEDDFs')
@@ -67,7 +67,7 @@ class DevengadoHorasExtrasDiariasDominicalesYFestivos(Devengado):
@dataclass
class DevengadoHorasRecargoDiariasDominicalesYFestivos(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HRDDFs')
@@ -76,7 +76,7 @@ class DevengadoHorasRecargoDiariasDominicalesYFestivos(Devengado):
@dataclass
class DevengadoHorasExtrasNocturnasDominicalesYFestivos(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HENDFs')
@@ -85,7 +85,7 @@ class DevengadoHorasExtrasNocturnasDominicalesYFestivos(Devengado):
@dataclass
class DevengadoHorasRecargoNocturnoDominicalesYFestivos(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HRNDFs')

View File

@@ -1,17 +1,15 @@
attrs==22.1.0
distlib==0.3.6
filelock==3.8.0
iniconfig==1.1.1
packaging==21.3
platformdirs==2.5.2
pluggy==1.0.0
py==1.11.0
pyparsing==3.0.9
pytest==7.1.3
attrs==24.2.0
distlib==0.3.9
filelock==3.16.1
iniconfig==2.0.0
packaging==24.1
platformdirs==4.3.6
pluggy==1.5.0
pyparsing==3.2.0
pytest==8.3.4
semantic-version==2.10.0
setuptools-rust==1.5.2
setuptools-rust==1.10.2
six==1.16.0
tomli==2.0.1
tox==3.26.0
typing_extensions==4.4.0
virtualenv==20.16.5
tomli==2.2.1
tox==4.23.2
virtualenv==20.28.0

View File

@@ -14,14 +14,14 @@ with open('HISTORY.rst') as history_file:
history = history_file.read()
requirements = ['Click>=8.1.7',
'zeep==4.2.1',
'lxml==5.2.2',
'cryptography==3.3.2',
'pyOpenSSL==20.0.1',
'xmlsig==0.1.7',
'xades==1.0.0',
'xmlsec==1.3.14',
'python-dateutil==2.9.0.post0',
'zeep>=4.2.1',
'lxml>=5.2.2',
'cryptography>=41.0.0',
'pyOpenSSL>=23.2.0',
'xmlsig>=0.1.9',
'xades>=1.0.0',
'xmlsec>=1.3.12',
'python-dateutil>=2.9.0',
# usamos esta dependencia en runtime
# para forzar uso de policy_id de archivo local
'mock>=5.1.0',
@@ -57,10 +57,10 @@ setup(
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Natural Language :: English',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
],
description="Facturacion Electronica Colombia",
entry_points={

View File

@@ -1,5 +1,5 @@
[tox]
envlist = py39, py310, py311, py312, flake8
envlist = py39, py310, py311, py312, py313, flake8
[travis]
python =
@@ -7,6 +7,7 @@ python =
3.10: py310
3.11: py311
3.12: py312
3.13: py313
[testenv:flake8]
basepython = python