attime-star-api-python-sdk 1.4.1__tar.gz → 1.9.0__tar.gz

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. {attime_star_api_python_sdk-1.4.1/src/attime_star_api_python_sdk.egg-info → attime_star_api_python_sdk-1.9.0}/PKG-INFO +1 -1
  2. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/pyproject.toml +1 -1
  3. attime_star_api_python_sdk-1.9.0/src/attime/star/apis/attime001.py +293 -0
  4. attime_star_api_python_sdk-1.9.0/src/attime/star/dtos/attime001.py +747 -0
  5. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/src/attime/star/dtos/bvix.py +9 -4
  6. attime_star_api_python_sdk-1.9.0/src/attime/star/dtos/ecommerce.py +684 -0
  7. attime_star_api_python_sdk-1.9.0/src/attime/star/dtos/enums.py +90 -0
  8. attime_star_api_python_sdk-1.9.0/src/attime/star/dtos/serde.py +56 -0
  9. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0/src/attime_star_api_python_sdk.egg-info}/PKG-INFO +1 -1
  10. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/src/attime_star_api_python_sdk.egg-info/SOURCES.txt +8 -1
  11. attime_star_api_python_sdk-1.9.0/tests/test_attime001_atualizacoes.py +123 -0
  12. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/tests/test_attime001_cancelamento.py +56 -13
  13. attime_star_api_python_sdk-1.9.0/tests/test_attime001_importar_ecommerce.py +158 -0
  14. attime_star_api_python_sdk-1.9.0/tests/test_attime001_insere_item.py +145 -0
  15. attime_star_api_python_sdk-1.9.0/tests/test_attime001_relacao.py +111 -0
  16. attime_star_api_python_sdk-1.4.1/src/attime/star/apis/attime001.py +0 -76
  17. attime_star_api_python_sdk-1.4.1/src/attime/star/dtos/attime001.py +0 -92
  18. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/LICENSE +0 -0
  19. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/README.md +0 -0
  20. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/setup.cfg +0 -0
  21. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/src/attime/star/api.py +0 -0
  22. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/src/attime/star/apis/__init__.py +0 -0
  23. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/src/attime/star/apis/bvix.py +0 -0
  24. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/src/attime/star/apis/infocar.py +0 -0
  25. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/src/attime/star/dtos/__init__.py +0 -0
  26. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/src/attime/star/dtos/infocar.py +0 -0
  27. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/src/attime_star_api_python_sdk.egg-info/dependency_links.txt +0 -0
  28. {attime_star_api_python_sdk-1.4.1 → attime_star_api_python_sdk-1.9.0}/src/attime_star_api_python_sdk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: attime-star-api-python-sdk
3
- Version: 1.4.1
3
+ Version: 1.9.0
4
4
  Summary: Client HTTP de comunicacao com a API da ATTime Star
5
5
  Author-email: Filipe Coelho <filipe@fmconsult.com.br>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "attime-star-api-python-sdk"
7
- version = "1.4.1"
7
+ version = "1.9.0"
8
8
  description = "Client HTTP de comunicacao com a API da ATTime Star"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,293 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Cliente HTTP ApiAttime001 (ABTP0004)."""
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Callable, TypeVar
7
+
8
+ import jsonpickle
9
+ from fmconsult.utils.url import UrlUtil
10
+ from http import HTTPMethod
11
+
12
+ from attime.star.api import ATTimeStarApi
13
+ from attime.star.dtos.attime001 import (
14
+ ApiAttime001AtualizacoesDTO,
15
+ ApiAttime001CancelamentoDTO,
16
+ ApiAttime001EmitirItemDTO,
17
+ ApiAttime001RelacaoContratoCpfItemDTO,
18
+ ApiAttime001RelacaoItemDTO,
19
+ )
20
+ from attime.star.dtos.ecommerce import ApiAttime001ImportarEcommerceDTO
21
+
22
+ T = TypeVar('T')
23
+
24
+
25
+ def _require_numero_contrato(payload: dict) -> None:
26
+ if payload.get('numeroContrato') in (None, ''):
27
+ raise ValueError('numeroContrato é obrigatório')
28
+
29
+
30
+ def _require_cancelamento_payload(payload: dict) -> None:
31
+ """Escopo via campos: declinio, ou identificadorItem ("" = contrato)."""
32
+ _require_numero_contrato(payload)
33
+ if payload.get('declinio'):
34
+ return
35
+ if 'identificadorItem' not in payload:
36
+ raise ValueError(
37
+ 'identificadorItem é obrigatório ("" para contrato; CAC 25/nº para item) '
38
+ 'ou informe o bloco declinio'
39
+ )
40
+
41
+
42
+ def _parse_attime001_list_response(
43
+ res: object,
44
+ *,
45
+ from_dict: Callable[[object], T | None],
46
+ endpoint_label: str,
47
+ ) -> list[T]:
48
+ """Normaliza lista / {dados} / {codigo} no padrão STAR BTC."""
49
+ if isinstance(res, list):
50
+ return [row for row in (from_dict(r) for r in res) if row is not None]
51
+
52
+ if isinstance(res, dict):
53
+ if 'dados' in res:
54
+ dados = res['dados']
55
+ if isinstance(dados, list):
56
+ return [
57
+ row for row in (from_dict(r) for r in dados) if row is not None
58
+ ]
59
+ if isinstance(dados, dict):
60
+ parsed = from_dict(dados)
61
+ return [parsed] if parsed is not None else []
62
+ raise Exception(res.get('mensagem', 'Resposta inesperada em dados'))
63
+
64
+ if 'codigo' in res:
65
+ raise Exception(res.get('mensagem', 'Erro desconhecido'))
66
+
67
+ parsed = from_dict(res)
68
+ return [parsed] if parsed is not None else []
69
+
70
+ raise Exception(f'Resposta inesperada da API Attime001 {endpoint_label}')
71
+
72
+
73
+ def _parse_attime001_object_response(
74
+ res: object,
75
+ *,
76
+ from_dict: Callable[[object], T | None],
77
+ endpoint_label: str,
78
+ ) -> T:
79
+ """Como a lista, mas exige exatamente um objeto (ex.: InsereItem)."""
80
+ rows = _parse_attime001_list_response(
81
+ res, from_dict=from_dict, endpoint_label=endpoint_label
82
+ )
83
+ if not rows:
84
+ raise Exception(
85
+ f'Resposta vazia da API Attime001 {endpoint_label}'
86
+ )
87
+ return rows[0]
88
+
89
+
90
+ def _require_emitir_item_keys(payload: dict) -> None:
91
+ for key in ('codigoProduto', 'codigoSucursal', 'sequencialContrato'):
92
+ if payload.get(key) in (None, ''):
93
+ raise ValueError(f'{key} é obrigatório')
94
+
95
+
96
+ class Attime001Api(ATTimeStarApi):
97
+ """Rotinas ApiAttime001 — emissão, manutenção e consultas Relacao."""
98
+
99
+ def cancelamento(
100
+ self, item: ApiAttime001CancelamentoDTO | dict
101
+ ) -> list[ApiAttime001CancelamentoDTO]:
102
+ """PUT /btc/ABTP0004/ApiAttime001/Cancelamento."""
103
+ logging.info('cancelling via ApiAttime001 Cancelamento...')
104
+ if isinstance(item, ApiAttime001CancelamentoDTO):
105
+ payload = item.to_dict()
106
+ else:
107
+ payload = dict(item)
108
+ # Compat: ignore legacy field if caller still sends it.
109
+ payload.pop('escopoOperacao', None)
110
+
111
+ _require_cancelamento_payload(payload)
112
+
113
+ url = UrlUtil().make_url(
114
+ self.base_url,
115
+ ['btc', 'ABTP0004', 'ApiAttime001', 'Cancelamento'],
116
+ )
117
+ res = self.call_request(
118
+ http_method=HTTPMethod.PUT,
119
+ request_url=url,
120
+ payload=payload,
121
+ )
122
+ res = jsonpickle.decode(res)
123
+ return _parse_attime001_list_response(
124
+ res,
125
+ from_dict=ApiAttime001CancelamentoDTO.from_dict,
126
+ endpoint_label='Cancelamento',
127
+ )
128
+
129
+ def atualizacoes(
130
+ self, item: ApiAttime001AtualizacoesDTO | dict
131
+ ) -> list[ApiAttime001AtualizacoesDTO]:
132
+ """PUT /btc/ABTP0004/ApiAttime001/Atualizacoes."""
133
+ logging.info('updating via ApiAttime001 Atualizacoes...')
134
+ if isinstance(item, ApiAttime001AtualizacoesDTO):
135
+ payload = item.to_dict()
136
+ else:
137
+ payload = dict(item)
138
+ payload.pop('escopoOperacao', None)
139
+
140
+ _require_numero_contrato(payload)
141
+
142
+ url = UrlUtil().make_url(
143
+ self.base_url,
144
+ ['btc', 'ABTP0004', 'ApiAttime001', 'Atualizacoes'],
145
+ )
146
+ res = self.call_request(
147
+ http_method=HTTPMethod.PUT,
148
+ request_url=url,
149
+ payload=payload,
150
+ )
151
+ res = jsonpickle.decode(res)
152
+ return _parse_attime001_list_response(
153
+ res,
154
+ from_dict=ApiAttime001AtualizacoesDTO.from_dict,
155
+ endpoint_label='Atualizacoes',
156
+ )
157
+
158
+ def insere_item(
159
+ self, item: ApiAttime001EmitirItemDTO | dict
160
+ ) -> ApiAttime001EmitirItemDTO:
161
+ """POST /btc/ABTP0004/ApiAttime001/InsereItem.
162
+
163
+ Disponível em homologação; produção ainda em liberação (STAR).
164
+ """
165
+ logging.info('inserting item via ApiAttime001 InsereItem...')
166
+ if isinstance(item, ApiAttime001EmitirItemDTO):
167
+ payload = item.to_dict()
168
+ else:
169
+ payload = dict(item)
170
+
171
+ _require_emitir_item_keys(payload)
172
+
173
+ url = UrlUtil().make_url(
174
+ self.base_url,
175
+ ['btc', 'ABTP0004', 'ApiAttime001', 'InsereItem'],
176
+ )
177
+ res = self.call_request(
178
+ http_method=HTTPMethod.POST,
179
+ request_url=url,
180
+ payload=payload,
181
+ )
182
+ res = jsonpickle.decode(res)
183
+ return _parse_attime001_object_response(
184
+ res,
185
+ from_dict=ApiAttime001EmitirItemDTO.from_dict,
186
+ endpoint_label='InsereItem',
187
+ )
188
+
189
+ def importar_ecommerce(
190
+ self, item: ApiAttime001ImportarEcommerceDTO | dict
191
+ ) -> ApiAttime001ImportarEcommerceDTO:
192
+ """POST /btc/ABTP0004/ApiAttime001/ImportarEcommerce."""
193
+ logging.info('importing ecommerce via ApiAttime001 ImportarEcommerce...')
194
+ if isinstance(item, ApiAttime001ImportarEcommerceDTO):
195
+ payload = item.to_dict()
196
+ else:
197
+ payload = dict(item)
198
+
199
+ if not payload.get('emissao'):
200
+ raise ValueError('emissao é obrigatório')
201
+ emissao = payload['emissao']
202
+ if isinstance(emissao, dict):
203
+ if not emissao.get('clientes'):
204
+ raise ValueError('emissao.clientes é obrigatório')
205
+ if not emissao.get('contratos'):
206
+ raise ValueError('emissao.contratos é obrigatório')
207
+
208
+ url = UrlUtil().make_url(
209
+ self.base_url,
210
+ ['btc', 'ABTP0004', 'ApiAttime001', 'ImportarEcommerce'],
211
+ )
212
+ res = self.call_request(
213
+ http_method=HTTPMethod.POST,
214
+ request_url=url,
215
+ payload=payload,
216
+ )
217
+ res = jsonpickle.decode(res)
218
+ return _parse_attime001_object_response(
219
+ res,
220
+ from_dict=ApiAttime001ImportarEcommerceDTO.from_dict,
221
+ endpoint_label='ImportarEcommerce',
222
+ )
223
+
224
+ def relacao_contrato(
225
+ self,
226
+ *,
227
+ cpf: str,
228
+ data_inicio_vigencia: str | None = None,
229
+ ramo: str | None = None,
230
+ ) -> list[ApiAttime001RelacaoContratoCpfItemDTO]:
231
+ """GET /btc/ABTP0004/ApiAttime001/RelacaoContrato.
232
+
233
+ Consulta contratos vinculados a um CPF (conferência pós-emissão).
234
+ """
235
+ logging.info('querying RelacaoContrato via ApiAttime001...')
236
+ if cpf in (None, ''):
237
+ raise ValueError('cpf é obrigatório')
238
+
239
+ params = {'Cpf': cpf}
240
+ if data_inicio_vigencia not in (None, ''):
241
+ params['DataInicioVigencia'] = data_inicio_vigencia
242
+ if ramo not in (None, ''):
243
+ params['Ramo'] = ramo
244
+
245
+ url = UrlUtil().make_url(
246
+ self.base_url,
247
+ ['btc', 'ABTP0004', 'ApiAttime001', 'RelacaoContrato'],
248
+ )
249
+ res = self.call_request(
250
+ http_method=HTTPMethod.GET,
251
+ request_url=url,
252
+ params=params,
253
+ )
254
+ res = jsonpickle.decode(res)
255
+ return _parse_attime001_list_response(
256
+ res,
257
+ from_dict=ApiAttime001RelacaoContratoCpfItemDTO.from_dict,
258
+ endpoint_label='RelacaoContrato',
259
+ )
260
+
261
+ def relacao_itens(
262
+ self,
263
+ *,
264
+ numero_contrato: str,
265
+ data_ref: str | None = None,
266
+ ) -> list[ApiAttime001RelacaoItemDTO]:
267
+ """GET /btc/ABTP0004/ApiAttime001/RelacaoItens.
268
+
269
+ Consulta itens de um contrato (conferência pós-emissão).
270
+ """
271
+ logging.info('querying RelacaoItens via ApiAttime001...')
272
+ if numero_contrato in (None, ''):
273
+ raise ValueError('numero_contrato é obrigatório')
274
+
275
+ params = {'NumeroContrato': numero_contrato}
276
+ if data_ref not in (None, ''):
277
+ params['DataRef'] = data_ref
278
+
279
+ url = UrlUtil().make_url(
280
+ self.base_url,
281
+ ['btc', 'ABTP0004', 'ApiAttime001', 'RelacaoItens'],
282
+ )
283
+ res = self.call_request(
284
+ http_method=HTTPMethod.GET,
285
+ request_url=url,
286
+ params=params,
287
+ )
288
+ res = jsonpickle.decode(res)
289
+ return _parse_attime001_list_response(
290
+ res,
291
+ from_dict=ApiAttime001RelacaoItemDTO.from_dict,
292
+ endpoint_label='RelacaoItens',
293
+ )