transdesk-importer-python-sdk 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transdesk/importer/__init__.py +70 -0
- transdesk/importer/client.py +42 -0
- transdesk/importer/exporters/__init__.py +3 -0
- transdesk/importer/exporters/json_exporter.py +25 -0
- transdesk/importer/models/__init__.py +59 -0
- transdesk/importer/models/canonical.py +124 -0
- transdesk/importer/models/internal.py +182 -0
- transdesk/importer/readers/__init__.py +3 -0
- transdesk/importer/readers/excel_reader.py +77 -0
- transdesk/importer/shared/__init__.py +3 -0
- transdesk/importer/shared/data_normalizer.py +47 -0
- transdesk/importer/transformers/__init__.py +0 -0
- transdesk/importer/transformers/canonical/__init__.py +5 -0
- transdesk/importer/transformers/canonical/apolice_builder.py +272 -0
- transdesk/importer/transformers/canonical/cobertura_builder.py +39 -0
- transdesk/importer/transformers/canonical/item_classifier.py +22 -0
- transdesk/importer/transformers/internal/__init__.py +6 -0
- transdesk/importer/transformers/internal/coverage_mapper.py +138 -0
- transdesk/importer/transformers/internal/internal_mapper.py +176 -0
- transdesk/importer/transformers/internal/rcf_parser.py +47 -0
- transdesk/importer/transformers/internal/towing_parser.py +13 -0
- transdesk/importer/validators/__init__.py +3 -0
- transdesk/importer/validators/internal_policy_validator.py +75 -0
- transdesk_importer_python_sdk-1.0.0.dist-info/METADATA +228 -0
- transdesk_importer_python_sdk-1.0.0.dist-info/RECORD +28 -0
- transdesk_importer_python_sdk-1.0.0.dist-info/WHEEL +5 -0
- transdesk_importer_python_sdk-1.0.0.dist-info/licenses/LICENSE +21 -0
- transdesk_importer_python_sdk-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""SDK de importacao de planilhas Transdesk.
|
|
2
|
+
|
|
3
|
+
Uso basico::
|
|
4
|
+
|
|
5
|
+
from transdesk.importer import TransdeskImporter
|
|
6
|
+
|
|
7
|
+
result = TransdeskImporter().import_policies(file_bytes)
|
|
8
|
+
result.policies # list[InternalPolicy]
|
|
9
|
+
result.errors # list[ValidationError]
|
|
10
|
+
result.to_dict() # dict pronto para JSON
|
|
11
|
+
"""
|
|
12
|
+
from transdesk.importer.client import TransdeskImporter
|
|
13
|
+
from transdesk.importer.models import (
|
|
14
|
+
Address,
|
|
15
|
+
Broker,
|
|
16
|
+
CanonicalItem,
|
|
17
|
+
CanonicalPolicy,
|
|
18
|
+
Cliente,
|
|
19
|
+
Cobertura,
|
|
20
|
+
Complemento,
|
|
21
|
+
CustomerInfo,
|
|
22
|
+
DadosBancarios,
|
|
23
|
+
Endereco,
|
|
24
|
+
ImportResult,
|
|
25
|
+
InternalItem,
|
|
26
|
+
InternalPolicy,
|
|
27
|
+
Lead,
|
|
28
|
+
Mobile,
|
|
29
|
+
PolicyData,
|
|
30
|
+
ReboqueRisco,
|
|
31
|
+
ResellerData,
|
|
32
|
+
Subestipulante,
|
|
33
|
+
Telephone,
|
|
34
|
+
UnidadeVenda,
|
|
35
|
+
ValidationError,
|
|
36
|
+
VeiculoRisco,
|
|
37
|
+
VidaRisco,
|
|
38
|
+
)
|
|
39
|
+
from transdesk.importer.readers.excel_reader import UnsupportedSpreadsheetError
|
|
40
|
+
|
|
41
|
+
__version__ = "1.0.0"
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"TransdeskImporter",
|
|
45
|
+
"UnsupportedSpreadsheetError",
|
|
46
|
+
"ImportResult",
|
|
47
|
+
"ValidationError",
|
|
48
|
+
"InternalPolicy",
|
|
49
|
+
"PolicyData",
|
|
50
|
+
"ResellerData",
|
|
51
|
+
"Broker",
|
|
52
|
+
"Lead",
|
|
53
|
+
"CustomerInfo",
|
|
54
|
+
"Telephone",
|
|
55
|
+
"Mobile",
|
|
56
|
+
"Address",
|
|
57
|
+
"InternalItem",
|
|
58
|
+
"CanonicalPolicy",
|
|
59
|
+
"CanonicalItem",
|
|
60
|
+
"Subestipulante",
|
|
61
|
+
"Cliente",
|
|
62
|
+
"Endereco",
|
|
63
|
+
"DadosBancarios",
|
|
64
|
+
"UnidadeVenda",
|
|
65
|
+
"Cobertura",
|
|
66
|
+
"Complemento",
|
|
67
|
+
"VeiculoRisco",
|
|
68
|
+
"ReboqueRisco",
|
|
69
|
+
"VidaRisco",
|
|
70
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Fachada publica do SDK.
|
|
2
|
+
|
|
3
|
+
Recebe a planilha (bytes / file-like / caminho), executa o pipeline
|
|
4
|
+
canonical -> internal -> validate e devolve um ``ImportResult`` tipado.
|
|
5
|
+
"""
|
|
6
|
+
from transdesk.importer.models.internal import ImportResult
|
|
7
|
+
from transdesk.importer.readers.excel_reader import ExcelReader
|
|
8
|
+
from transdesk.importer.transformers.canonical.apolice_builder import ApoliceBuilder
|
|
9
|
+
from transdesk.importer.transformers.internal.internal_mapper import InternalMapper
|
|
10
|
+
from transdesk.importer.validators.internal_policy_validator import InternalPolicyValidator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TransdeskImporter:
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.reader = ExcelReader()
|
|
17
|
+
self.canonical_builder = ApoliceBuilder()
|
|
18
|
+
self.mapper = InternalMapper()
|
|
19
|
+
self.validator = InternalPolicyValidator()
|
|
20
|
+
|
|
21
|
+
def import_policies(self, file) -> ImportResult:
|
|
22
|
+
"""Le a planilha e devolve as apolices no formato interno.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
file: ``bytes``/``bytearray``, objeto file-like binario ou caminho
|
|
26
|
+
de arquivo (``str``/``os.PathLike``).
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
ImportResult: ``policies`` (list[InternalPolicy]) e ``errors``
|
|
30
|
+
(list[ValidationError]).
|
|
31
|
+
"""
|
|
32
|
+
df = self.reader.read(file)
|
|
33
|
+
|
|
34
|
+
canonical_policies = self.canonical_builder.build(df)
|
|
35
|
+
internal_policies = [
|
|
36
|
+
self.mapper.map(policy)
|
|
37
|
+
for policy in canonical_policies
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
errors = self.validator.validate(canonical_policies, internal_policies)
|
|
41
|
+
|
|
42
|
+
return ImportResult(policies=internal_policies, errors=errors)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Utilitario de desenvolvimento/debug para gravar resultados em JSON.
|
|
2
|
+
|
|
3
|
+
Nao faz parte do caminho principal do SDK (que retorna models tipados), mas
|
|
4
|
+
ajuda a inspecionar saidas durante o desenvolvimento. Lida com dataclasses,
|
|
5
|
+
listas e ``ImportResult``.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import asdict, is_dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _default(obj):
|
|
12
|
+
if is_dataclass(obj) and not isinstance(obj, type):
|
|
13
|
+
return asdict(obj)
|
|
14
|
+
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JsonExporter:
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def export(data, output_file):
|
|
21
|
+
if hasattr(data, "to_dict"):
|
|
22
|
+
data = data.to_dict()
|
|
23
|
+
|
|
24
|
+
with open(output_file, "w", encoding="utf8") as f:
|
|
25
|
+
json.dump(data, f, ensure_ascii=False, indent=4, default=_default)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from .canonical import (
|
|
2
|
+
CanonicalItem,
|
|
3
|
+
CanonicalPolicy,
|
|
4
|
+
Cliente,
|
|
5
|
+
Cobertura,
|
|
6
|
+
Complemento,
|
|
7
|
+
DadosBancarios,
|
|
8
|
+
DadosRisco,
|
|
9
|
+
Endereco,
|
|
10
|
+
ReboqueRisco,
|
|
11
|
+
Subestipulante,
|
|
12
|
+
UnidadeVenda,
|
|
13
|
+
VeiculoRisco,
|
|
14
|
+
VidaRisco,
|
|
15
|
+
)
|
|
16
|
+
from .internal import (
|
|
17
|
+
Address,
|
|
18
|
+
Broker,
|
|
19
|
+
CustomerInfo,
|
|
20
|
+
ImportResult,
|
|
21
|
+
InternalItem,
|
|
22
|
+
InternalPolicy,
|
|
23
|
+
Lead,
|
|
24
|
+
Mobile,
|
|
25
|
+
PolicyData,
|
|
26
|
+
ResellerData,
|
|
27
|
+
Telephone,
|
|
28
|
+
ValidationError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
# canonical
|
|
33
|
+
"CanonicalItem",
|
|
34
|
+
"CanonicalPolicy",
|
|
35
|
+
"Cliente",
|
|
36
|
+
"Cobertura",
|
|
37
|
+
"Complemento",
|
|
38
|
+
"DadosBancarios",
|
|
39
|
+
"DadosRisco",
|
|
40
|
+
"Endereco",
|
|
41
|
+
"ReboqueRisco",
|
|
42
|
+
"Subestipulante",
|
|
43
|
+
"UnidadeVenda",
|
|
44
|
+
"VeiculoRisco",
|
|
45
|
+
"VidaRisco",
|
|
46
|
+
# internal
|
|
47
|
+
"Address",
|
|
48
|
+
"Broker",
|
|
49
|
+
"CustomerInfo",
|
|
50
|
+
"ImportResult",
|
|
51
|
+
"InternalItem",
|
|
52
|
+
"InternalPolicy",
|
|
53
|
+
"Lead",
|
|
54
|
+
"Mobile",
|
|
55
|
+
"PolicyData",
|
|
56
|
+
"ResellerData",
|
|
57
|
+
"Telephone",
|
|
58
|
+
"ValidationError",
|
|
59
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Models da camada canonica (representacao intermediaria, em PT-BR).
|
|
2
|
+
|
|
3
|
+
Espelham a estrutura produzida a partir da planilha, antes do mapeamento
|
|
4
|
+
para o formato interno consumido pela core-api.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import List, Optional, Union
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Cobertura:
|
|
14
|
+
nome: Optional[str] = None
|
|
15
|
+
tipo: Optional[str] = None
|
|
16
|
+
combo: Optional[str] = None
|
|
17
|
+
produto: Optional[str] = None
|
|
18
|
+
categoria: Optional[str] = None
|
|
19
|
+
tipo_categoria: Optional[str] = None
|
|
20
|
+
codigo_tabela: Optional[str] = None
|
|
21
|
+
franquia: Optional[float] = None
|
|
22
|
+
importancia_segurada: Optional[float] = None
|
|
23
|
+
valor_mensalidade: Optional[float] = None
|
|
24
|
+
observacao: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Complemento:
|
|
29
|
+
descricao: Optional[str] = None
|
|
30
|
+
franquia: Optional[float] = None
|
|
31
|
+
importancia_segurada: Optional[float] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class VeiculoRisco:
|
|
36
|
+
placa: Optional[str] = None
|
|
37
|
+
chassi: Optional[str] = None
|
|
38
|
+
codigo_fipe: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ReboqueRisco:
|
|
43
|
+
placa: Optional[str] = None
|
|
44
|
+
chassi: Optional[str] = None
|
|
45
|
+
marca: Optional[str] = None
|
|
46
|
+
modelo: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class VidaRisco:
|
|
51
|
+
nome: Optional[str] = None
|
|
52
|
+
cpf: Optional[str] = None
|
|
53
|
+
rg: Optional[str] = None
|
|
54
|
+
data_nascimento: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
DadosRisco = Union[VeiculoRisco, ReboqueRisco, VidaRisco]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class CanonicalItem:
|
|
62
|
+
tipo_item: str
|
|
63
|
+
dados_risco: DadosRisco
|
|
64
|
+
coberturas: List[Cobertura] = field(default_factory=list)
|
|
65
|
+
complementos: Optional[List[Complemento]] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Endereco:
|
|
70
|
+
cep: Optional[str] = None
|
|
71
|
+
logradouro: Optional[str] = None
|
|
72
|
+
numero: Optional[str] = None
|
|
73
|
+
complemento: Optional[str] = None
|
|
74
|
+
bairro: Optional[str] = None
|
|
75
|
+
cidade: Optional[str] = None
|
|
76
|
+
uf: Optional[str] = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class DadosBancarios:
|
|
81
|
+
banco: Optional[str] = None
|
|
82
|
+
agencia: Optional[str] = None
|
|
83
|
+
conta: Optional[str] = None
|
|
84
|
+
chave_pix: Optional[str] = None
|
|
85
|
+
titular: Optional[str] = None
|
|
86
|
+
documento_titular: Optional[str] = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class Cliente:
|
|
91
|
+
matricula: Optional[str] = None
|
|
92
|
+
tipo_pessoa: Optional[str] = None
|
|
93
|
+
documento: Optional[str] = None
|
|
94
|
+
nome_fantasia: Optional[str] = None
|
|
95
|
+
razao_social: Optional[str] = None
|
|
96
|
+
telefones: List[Optional[str]] = field(default_factory=list)
|
|
97
|
+
emails: List[Optional[str]] = field(default_factory=list)
|
|
98
|
+
endereco: Endereco = field(default_factory=Endereco)
|
|
99
|
+
dados_bancarios: DadosBancarios = field(default_factory=DadosBancarios)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class Subestipulante:
|
|
104
|
+
id_unity: Optional[str] = None
|
|
105
|
+
documento: Optional[str] = None
|
|
106
|
+
nome_fantasia: Optional[str] = None
|
|
107
|
+
razao_social: Optional[str] = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class UnidadeVenda:
|
|
112
|
+
documento: Optional[str] = None
|
|
113
|
+
nome_fantasia: Optional[str] = None
|
|
114
|
+
razao_social: Optional[str] = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class CanonicalPolicy:
|
|
119
|
+
cod_agrupador_apolice: int
|
|
120
|
+
dia_vencimento: Optional[int]
|
|
121
|
+
subestipulante: Subestipulante
|
|
122
|
+
cliente: Cliente
|
|
123
|
+
unidade_venda: UnidadeVenda
|
|
124
|
+
itens: List[CanonicalItem] = field(default_factory=list)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Models da camada interna (contrato publico/output do SDK, em EN).
|
|
2
|
+
|
|
3
|
+
Esta e a estrutura entregue para quem consome o SDK (ex.: a core-api).
|
|
4
|
+
A serializacao para JSON e feita via ``dataclasses.asdict`` (helpers em
|
|
5
|
+
``ImportResult``).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import asdict, dataclass, field
|
|
11
|
+
from typing import Any, Dict, List, Optional, Union
|
|
12
|
+
|
|
13
|
+
from .canonical import CanonicalPolicy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Broker:
|
|
18
|
+
internal_code: Optional[str] = None
|
|
19
|
+
document_number: Optional[str] = None
|
|
20
|
+
principal_name: Optional[str] = None
|
|
21
|
+
secondary_name: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class CustomerInfo:
|
|
26
|
+
cnpj: Optional[str] = None
|
|
27
|
+
principal_name: Optional[str] = None
|
|
28
|
+
secondary_name: Optional[str] = None
|
|
29
|
+
email_address: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Telephone:
|
|
34
|
+
telphone_type_code: str
|
|
35
|
+
ddd: str
|
|
36
|
+
number: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Mobile:
|
|
41
|
+
telphone_type_code: str
|
|
42
|
+
ddd: str
|
|
43
|
+
number: str
|
|
44
|
+
allow_sms: bool = True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Address:
|
|
49
|
+
type_code: Optional[str] = None
|
|
50
|
+
cep: Optional[str] = None
|
|
51
|
+
name: Optional[str] = None
|
|
52
|
+
number: Optional[str] = None
|
|
53
|
+
complement: Optional[str] = None
|
|
54
|
+
district: Optional[str] = None
|
|
55
|
+
city: Optional[str] = None
|
|
56
|
+
acronym_federal_unit: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Lead:
|
|
61
|
+
customer_info: CustomerInfo
|
|
62
|
+
telphone_info: Optional[Telephone] = None
|
|
63
|
+
celphone_info: Optional[Mobile] = None
|
|
64
|
+
address_info: Address = field(default_factory=Address)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class InternalItem:
|
|
69
|
+
item_number: int
|
|
70
|
+
item_type: str
|
|
71
|
+
|
|
72
|
+
# Casco
|
|
73
|
+
has_casco_cir: bool = False
|
|
74
|
+
hull_value: float = 0
|
|
75
|
+
hull_premium: float = 0
|
|
76
|
+
|
|
77
|
+
# Carroceria / Complemento / Equipamento
|
|
78
|
+
body_value: float = 0
|
|
79
|
+
body_premium: float = 0
|
|
80
|
+
|
|
81
|
+
# Reparos
|
|
82
|
+
has_repair_assistance: bool = False
|
|
83
|
+
repair_assistance_value: float = 0
|
|
84
|
+
repair_assistance_premium: float = 0
|
|
85
|
+
|
|
86
|
+
# RCF
|
|
87
|
+
has_rcf: bool = False
|
|
88
|
+
property_damage_value: float = 0
|
|
89
|
+
property_damage_premium: float = 0
|
|
90
|
+
bodily_injury_value: float = 0
|
|
91
|
+
bodily_injury_premium: float = 0
|
|
92
|
+
moral_damage_value: float = 0
|
|
93
|
+
moral_damage_premium: float = 0
|
|
94
|
+
|
|
95
|
+
# APP
|
|
96
|
+
has_app: bool = False
|
|
97
|
+
personal_accident_coverage_value: float = 0
|
|
98
|
+
personal_accident_coverage_premium: float = 0
|
|
99
|
+
|
|
100
|
+
# Guincho
|
|
101
|
+
towing_km: float = 0
|
|
102
|
+
towing_premium: float = 0
|
|
103
|
+
|
|
104
|
+
# Vidros
|
|
105
|
+
glass_coverage_type: Optional[str] = None
|
|
106
|
+
glass_premium: float = 0
|
|
107
|
+
|
|
108
|
+
# Rastreador
|
|
109
|
+
has_vehicle_tracker: bool = False
|
|
110
|
+
vehicle_tracker_premium: float = 0
|
|
111
|
+
|
|
112
|
+
# Juridica
|
|
113
|
+
has_legal_assistance: bool = False
|
|
114
|
+
legal_assistance_value: float = 0
|
|
115
|
+
legal_assistance_premium: float = 0
|
|
116
|
+
|
|
117
|
+
# Combo
|
|
118
|
+
group_name: Optional[str] = None
|
|
119
|
+
|
|
120
|
+
# Veiculo / Reboque
|
|
121
|
+
license_plate: Optional[str] = None
|
|
122
|
+
chassi_number: Optional[str] = None
|
|
123
|
+
fipe_code: Optional[str] = None
|
|
124
|
+
body_description: Optional[str] = None
|
|
125
|
+
brand: Optional[str] = None
|
|
126
|
+
model: Optional[str] = None
|
|
127
|
+
|
|
128
|
+
# Vida
|
|
129
|
+
name: Optional[str] = None
|
|
130
|
+
cpf: Optional[str] = None
|
|
131
|
+
rg: Optional[str] = None
|
|
132
|
+
birth_date: Optional[str] = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class ResellerData:
|
|
137
|
+
broker: Broker
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class PolicyData:
|
|
142
|
+
reseller: ResellerData
|
|
143
|
+
lead: Lead
|
|
144
|
+
items: List[InternalItem] = field(default_factory=list)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class InternalPolicy:
|
|
149
|
+
reference_id: str
|
|
150
|
+
payment_due_date: Optional[int]
|
|
151
|
+
data: PolicyData
|
|
152
|
+
original_data: CanonicalPolicy
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class ValidationError:
|
|
157
|
+
type: str
|
|
158
|
+
policy: Optional[Union[int, str]] = None
|
|
159
|
+
item: Optional[int] = None
|
|
160
|
+
canonical: Optional[float] = None
|
|
161
|
+
internal: Optional[float] = None
|
|
162
|
+
difference: Optional[float] = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class ImportResult:
|
|
167
|
+
policies: List[InternalPolicy] = field(default_factory=list)
|
|
168
|
+
errors: List[ValidationError] = field(default_factory=list)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def is_valid(self) -> bool:
|
|
172
|
+
return not self.errors
|
|
173
|
+
|
|
174
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
175
|
+
return {
|
|
176
|
+
"policies": [asdict(policy) for policy in self.policies],
|
|
177
|
+
"errors": [asdict(error) for error in self.errors],
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
def to_json(self, **kwargs: Any) -> str:
|
|
181
|
+
kwargs.setdefault("ensure_ascii", False)
|
|
182
|
+
return json.dumps(self.to_dict(), **kwargs)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Leitura da planilha de apolices.
|
|
2
|
+
|
|
3
|
+
Aceita:
|
|
4
|
+
- ``bytes`` / ``bytearray`` (upload em memoria);
|
|
5
|
+
- objeto file-like binario (ex.: ``io.BytesIO``, ``request.files['file'].file``);
|
|
6
|
+
- caminho de arquivo (``str`` / ``os.PathLike``).
|
|
7
|
+
|
|
8
|
+
O formato (``.xls`` vs ``.xlsx``) e detectado por magic bytes, de modo que
|
|
9
|
+
nenhuma extensao de arquivo e necessaria quando a planilha chega como bytes.
|
|
10
|
+
"""
|
|
11
|
+
import io
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
# Assinaturas (magic bytes)
|
|
18
|
+
_XLSX_SIGNATURE = b"PK\x03\x04" # ZIP (OOXML / .xlsx)
|
|
19
|
+
_XLS_SIGNATURE = b"\xd0\xcf\x11\xe0" # OLE2 Compound File (.xls)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnsupportedSpreadsheetError(Exception):
|
|
23
|
+
"""Lancada quando o conteudo nao corresponde a um .xls ou .xlsx valido."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExcelReader:
|
|
27
|
+
|
|
28
|
+
def read(self, file):
|
|
29
|
+
buffer, engine = self._resolve(file)
|
|
30
|
+
return pd.read_excel(buffer, dtype=str, engine=engine).fillna("")
|
|
31
|
+
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
# internals
|
|
34
|
+
# ------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def _resolve(self, file):
|
|
37
|
+
if isinstance(file, (bytes, bytearray)):
|
|
38
|
+
data = bytes(file)
|
|
39
|
+
return io.BytesIO(data), self._engine_from_signature(data[:8])
|
|
40
|
+
|
|
41
|
+
if isinstance(file, (str, os.PathLike)):
|
|
42
|
+
return str(file), self._engine_from_path(file)
|
|
43
|
+
|
|
44
|
+
# file-like binario
|
|
45
|
+
if hasattr(file, "read"):
|
|
46
|
+
head = file.read(8)
|
|
47
|
+
if hasattr(file, "seek"):
|
|
48
|
+
file.seek(0)
|
|
49
|
+
return file, self._engine_from_signature(head)
|
|
50
|
+
|
|
51
|
+
raise UnsupportedSpreadsheetError(
|
|
52
|
+
f"Tipo de entrada nao suportado: {type(file)!r}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _engine_from_signature(self, head):
|
|
56
|
+
head = bytes(head or b"")
|
|
57
|
+
|
|
58
|
+
if head.startswith(_XLSX_SIGNATURE):
|
|
59
|
+
return "openpyxl"
|
|
60
|
+
|
|
61
|
+
if head.startswith(_XLS_SIGNATURE):
|
|
62
|
+
return "xlrd"
|
|
63
|
+
|
|
64
|
+
raise UnsupportedSpreadsheetError(
|
|
65
|
+
"Conteudo nao reconhecido como .xls (OLE2) nem .xlsx (OOXML)."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _engine_from_path(self, file_path):
|
|
69
|
+
ext = Path(file_path).suffix.lower()
|
|
70
|
+
|
|
71
|
+
if ext == ".xlsx":
|
|
72
|
+
return "openpyxl"
|
|
73
|
+
|
|
74
|
+
if ext == ".xls":
|
|
75
|
+
return "xlrd"
|
|
76
|
+
|
|
77
|
+
raise UnsupportedSpreadsheetError(f"Formato nao suportado: {ext}")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class DataNormalizer:
|
|
2
|
+
|
|
3
|
+
@staticmethod
|
|
4
|
+
def clean_doc(value):
|
|
5
|
+
if value is None:
|
|
6
|
+
return None
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
str(value)
|
|
10
|
+
.replace("*", "")
|
|
11
|
+
.replace(".", "")
|
|
12
|
+
.replace("-", "")
|
|
13
|
+
.replace("/", "")
|
|
14
|
+
.strip()
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def to_float(value):
|
|
19
|
+
if value in (None, ""):
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
if isinstance(value, (int, float)):
|
|
23
|
+
return float(value)
|
|
24
|
+
|
|
25
|
+
value = str(value).strip()
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# formato BR
|
|
29
|
+
if "," in value:
|
|
30
|
+
return float(
|
|
31
|
+
value
|
|
32
|
+
.replace(".", "")
|
|
33
|
+
.replace(",", ".")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# formato ja decimal
|
|
37
|
+
return float(value)
|
|
38
|
+
|
|
39
|
+
except Exception:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def to_int(value):
|
|
44
|
+
try:
|
|
45
|
+
return int(value)
|
|
46
|
+
except Exception:
|
|
47
|
+
return None
|
|
File without changes
|