1 Commits

Author SHA1 Message Date
93a3d18e32 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
2026-07-02 22:45:33 -05:00
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 && add-apt-repository ppa:deadsnakes/ppa
RUN apt-get install -y --no-install-recommends \ 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.10 python3.10-distutils python3.10-dev \
python3.11 python3.11-distutils python3.11-dev \ python3.11 python3.11-distutils python3.11-dev \
python3.12 python3-setuptools python3.12-dev \ python3.12 python3-setuptools python3.12-dev \
python3.13 python3-setuptools python3.13-dev \
wget \ wget \
ca-certificates ca-certificates
RUN wget https://bootstrap.pypa.io/get-pip.py \ 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.10 get-pip.py pip==23.2.1 --break-system-packages \
&& python3.11 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.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 && rm get-pip.py
RUN apt-get install -y --no-install-recommends \ RUN apt-get install -y --no-install-recommends \
libxml2-dev \ gcc \
libxmlsec1-dev \ build-essential \
build-essential \ pkg-config \
libxml2-dev \
libxmlsec1-dev \
xmlsec1 \
libxml2-dev \
libxslt-dev \
zip zip
RUN python3.9 --version
RUN python3.10 --version RUN python3.10 --version
RUN python3.11 --version RUN python3.11 --version
RUN python3.12 --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.10 install setuptools setuptools-rust
RUN pip3.11 install setuptools setuptools-rust --break-system-packages RUN pip3.11 install setuptools setuptools-rust --break-system-packages
RUN pip3.12 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 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 docker run --rm -ti -v "$(PWD):/app" -w /app --name facho-cli facho bash
test: 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: tox:
docker run -it -v $(PWD)/:/app -w /app facho 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['pretty_print'] = attrs.pop('pretty_print', False)
attrs['encoding'] = attrs.pop('encoding', 'UTF-8') 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()) keys = filter(lambda key: key.startswith('facho_'), el.keys())
self.remove_attributes(el, keys, exclude=['facho_optional']) self.remove_attributes(el, keys, exclude=['facho_optional'])
is_optional = el.get('facho_optional', 'False') == 'True' 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']: 'facho_optional']:
el.getparent().remove(el) el.getparent().remove(el)
@@ -291,7 +291,7 @@ class FachoXML:
# se fuerza la adicion como un nuevo elemento # se fuerza la adicion como un nuevo elemento
if append: if append:
last_slibing = None last_slibing = None
for child in parent.getchildren(): for child in list(parent):
if child.tag == node_tag: if child.tag == node_tag:
last_slibing = child last_slibing = child

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import uuid
import xmlsig import xmlsig
import xades import xades
from datetime import datetime from datetime import datetime
import OpenSSL
import zipfile import zipfile
# import warnings # import warnings
import hashlib import hashlib
@@ -18,6 +17,21 @@ from dateutil import tz
from cryptography.hazmat.primitives.serialization import pkcs12 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_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code']
AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code'] AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code']
@@ -444,9 +458,6 @@ class DianXMLExtensionSigner:
self._pkcs12_data, self._pkcs12_data,
self._passphrase)) self._passphrase))
# ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(
# self._pkcs12_data,
# self._passphrase))
if self._localpolicy: if self._localpolicy:
with mock_xades_policy(): with mock_xades_policy():
ctx.sign(signature) ctx.sign(signature)
@@ -590,8 +601,8 @@ class DianXMLExtensionSignerVerifier:
if isinstance(self._pkcs12_path_or_bytes, str): if isinstance(self._pkcs12_path_or_bytes, str):
pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read() pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read()
ctx = xades.XAdESContext() ctx = xades.XAdESContext()
ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(pkcs12_data, ctx.load_pkcs12(pkcs12.load_key_and_certificates(
self._passphrase)) pkcs12_data, self._passphrase))
try: try:
if self._localpolicy: if self._localpolicy:
with mock_xades_policy(): with mock_xades_policy():

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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