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.
Files changed (28) hide show
  1. transdesk/importer/__init__.py +70 -0
  2. transdesk/importer/client.py +42 -0
  3. transdesk/importer/exporters/__init__.py +3 -0
  4. transdesk/importer/exporters/json_exporter.py +25 -0
  5. transdesk/importer/models/__init__.py +59 -0
  6. transdesk/importer/models/canonical.py +124 -0
  7. transdesk/importer/models/internal.py +182 -0
  8. transdesk/importer/readers/__init__.py +3 -0
  9. transdesk/importer/readers/excel_reader.py +77 -0
  10. transdesk/importer/shared/__init__.py +3 -0
  11. transdesk/importer/shared/data_normalizer.py +47 -0
  12. transdesk/importer/transformers/__init__.py +0 -0
  13. transdesk/importer/transformers/canonical/__init__.py +5 -0
  14. transdesk/importer/transformers/canonical/apolice_builder.py +272 -0
  15. transdesk/importer/transformers/canonical/cobertura_builder.py +39 -0
  16. transdesk/importer/transformers/canonical/item_classifier.py +22 -0
  17. transdesk/importer/transformers/internal/__init__.py +6 -0
  18. transdesk/importer/transformers/internal/coverage_mapper.py +138 -0
  19. transdesk/importer/transformers/internal/internal_mapper.py +176 -0
  20. transdesk/importer/transformers/internal/rcf_parser.py +47 -0
  21. transdesk/importer/transformers/internal/towing_parser.py +13 -0
  22. transdesk/importer/validators/__init__.py +3 -0
  23. transdesk/importer/validators/internal_policy_validator.py +75 -0
  24. transdesk_importer_python_sdk-1.0.0.dist-info/METADATA +228 -0
  25. transdesk_importer_python_sdk-1.0.0.dist-info/RECORD +28 -0
  26. transdesk_importer_python_sdk-1.0.0.dist-info/WHEEL +5 -0
  27. transdesk_importer_python_sdk-1.0.0.dist-info/licenses/LICENSE +21 -0
  28. 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,3 @@
1
+ from .json_exporter import JsonExporter
2
+
3
+ __all__ = ["JsonExporter"]
@@ -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,3 @@
1
+ from .excel_reader import ExcelReader, UnsupportedSpreadsheetError
2
+
3
+ __all__ = ["ExcelReader", "UnsupportedSpreadsheetError"]
@@ -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,3 @@
1
+ from .data_normalizer import DataNormalizer
2
+
3
+ __all__ = ["DataNormalizer"]
@@ -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
@@ -0,0 +1,5 @@
1
+ from .apolice_builder import ApoliceBuilder
2
+ from .cobertura_builder import CoberturaBuilder
3
+ from .item_classifier import ItemClassifier
4
+
5
+ __all__ = ["ApoliceBuilder", "CoberturaBuilder", "ItemClassifier"]