internal-lib-48e662a6 0.1.0a5__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.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: internal-lib-48e662a6
3
+ Version: 0.1.0a5
4
+ Summary: Internal utility package
5
+ Project-URL: Homepage, https://github.com/seu-usuario/sage_x3_requests
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: httpx
12
+ Requires-Dist: pydantic
13
+
14
+ # Sage X3 Requests Wrapper (ALPHA)
15
+
16
+ > ⚠️ **AVISO: BIBLIOTECA EM DESENVOLVIMENTO (ALPHA)** ⚠️
17
+ >
18
+ > Esta biblioteca foi publicada para uso pessoal e testes. **Não é recomendada para produção.**
19
+ > A API pode mudar drasticamente a qualquer momento sem aviso prévio. Use por sua conta e risco.
20
+
21
+ ## Instalação
22
+
23
+ ```bash
24
+ pip install internal-lib-48e662a6
25
+ ```
26
+
27
+ ## Exemplos de Uso
28
+
29
+ ### Configuração
30
+ ```python
31
+ from sage_x3_requests import SageX3Config, SageX3Requester
32
+
33
+ config = SageX3Config(
34
+ base_url="https://seu-erp.com",
35
+ username="admin",
36
+ password="password",
37
+ folder="SEED"
38
+ )
39
+ ```
40
+
41
+ ### Pedir 5 Registos (Limit)
42
+ Pode passar o limite diretamente no método `.get_resources(limit=5)`.
43
+
44
+ ```python
45
+ with SageX3Requester(config) as client:
46
+ # Opção 1: Passar limit no get_resources (Maneira mais simples!)
47
+ registos = client.request("CLIENTES", "BPC") \
48
+ .get_resources(limit=5)
49
+
50
+ # Opção 2: Usar .count(5) antes
51
+ registos = client.request("CLIENTES", "BPC") \
52
+ .count(5) \
53
+ .get_resources()
54
+ ```
55
+
56
+ ### Pedir TODOS os Registos (Paginação Automática)
57
+ Use `.get_resources(fetch_all=True)` para buscar todos os dados, percorrendo todas as páginas automaticamente.
58
+
59
+ ```python
60
+ with SageX3Requester(config) as client:
61
+ # Busca todos os clientes (cuidado com grandes volumes de dados!)
62
+ todos_clientes = client.request("CLIENTES", "BPC") \
63
+ .get_resources(fetch_all=True)
64
+
65
+ print(f"Total encontrado: {len(todos_clientes)}")
66
+ ```
67
+
68
+ ### Contagem Total
69
+ Para saber quantos registos existem sem trazer os dados:
70
+
71
+ ```python
72
+ with SageX3Requester(config) as client:
73
+ query = client.request("CLIENTES", "BPC").count(True).execute()
74
+ total = query.get("count") # ou verificar estrutura de retorno
75
+ ```
@@ -0,0 +1,62 @@
1
+ # Sage X3 Requests Wrapper (ALPHA)
2
+
3
+ > ⚠️ **AVISO: BIBLIOTECA EM DESENVOLVIMENTO (ALPHA)** ⚠️
4
+ >
5
+ > Esta biblioteca foi publicada para uso pessoal e testes. **Não é recomendada para produção.**
6
+ > A API pode mudar drasticamente a qualquer momento sem aviso prévio. Use por sua conta e risco.
7
+
8
+ ## Instalação
9
+
10
+ ```bash
11
+ pip install internal-lib-48e662a6
12
+ ```
13
+
14
+ ## Exemplos de Uso
15
+
16
+ ### Configuração
17
+ ```python
18
+ from sage_x3_requests import SageX3Config, SageX3Requester
19
+
20
+ config = SageX3Config(
21
+ base_url="https://seu-erp.com",
22
+ username="admin",
23
+ password="password",
24
+ folder="SEED"
25
+ )
26
+ ```
27
+
28
+ ### Pedir 5 Registos (Limit)
29
+ Pode passar o limite diretamente no método `.get_resources(limit=5)`.
30
+
31
+ ```python
32
+ with SageX3Requester(config) as client:
33
+ # Opção 1: Passar limit no get_resources (Maneira mais simples!)
34
+ registos = client.request("CLIENTES", "BPC") \
35
+ .get_resources(limit=5)
36
+
37
+ # Opção 2: Usar .count(5) antes
38
+ registos = client.request("CLIENTES", "BPC") \
39
+ .count(5) \
40
+ .get_resources()
41
+ ```
42
+
43
+ ### Pedir TODOS os Registos (Paginação Automática)
44
+ Use `.get_resources(fetch_all=True)` para buscar todos os dados, percorrendo todas as páginas automaticamente.
45
+
46
+ ```python
47
+ with SageX3Requester(config) as client:
48
+ # Busca todos os clientes (cuidado com grandes volumes de dados!)
49
+ todos_clientes = client.request("CLIENTES", "BPC") \
50
+ .get_resources(fetch_all=True)
51
+
52
+ print(f"Total encontrado: {len(todos_clientes)}")
53
+ ```
54
+
55
+ ### Contagem Total
56
+ Para saber quantos registos existem sem trazer os dados:
57
+
58
+ ```python
59
+ with SageX3Requester(config) as client:
60
+ query = client.request("CLIENTES", "BPC").count(True).execute()
61
+ total = query.get("count") # ou verificar estrutura de retorno
62
+ ```
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: internal-lib-48e662a6
3
+ Version: 0.1.0a5
4
+ Summary: Internal utility package
5
+ Project-URL: Homepage, https://github.com/seu-usuario/sage_x3_requests
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: httpx
12
+ Requires-Dist: pydantic
13
+
14
+ # Sage X3 Requests Wrapper (ALPHA)
15
+
16
+ > ⚠️ **AVISO: BIBLIOTECA EM DESENVOLVIMENTO (ALPHA)** ⚠️
17
+ >
18
+ > Esta biblioteca foi publicada para uso pessoal e testes. **Não é recomendada para produção.**
19
+ > A API pode mudar drasticamente a qualquer momento sem aviso prévio. Use por sua conta e risco.
20
+
21
+ ## Instalação
22
+
23
+ ```bash
24
+ pip install internal-lib-48e662a6
25
+ ```
26
+
27
+ ## Exemplos de Uso
28
+
29
+ ### Configuração
30
+ ```python
31
+ from sage_x3_requests import SageX3Config, SageX3Requester
32
+
33
+ config = SageX3Config(
34
+ base_url="https://seu-erp.com",
35
+ username="admin",
36
+ password="password",
37
+ folder="SEED"
38
+ )
39
+ ```
40
+
41
+ ### Pedir 5 Registos (Limit)
42
+ Pode passar o limite diretamente no método `.get_resources(limit=5)`.
43
+
44
+ ```python
45
+ with SageX3Requester(config) as client:
46
+ # Opção 1: Passar limit no get_resources (Maneira mais simples!)
47
+ registos = client.request("CLIENTES", "BPC") \
48
+ .get_resources(limit=5)
49
+
50
+ # Opção 2: Usar .count(5) antes
51
+ registos = client.request("CLIENTES", "BPC") \
52
+ .count(5) \
53
+ .get_resources()
54
+ ```
55
+
56
+ ### Pedir TODOS os Registos (Paginação Automática)
57
+ Use `.get_resources(fetch_all=True)` para buscar todos os dados, percorrendo todas as páginas automaticamente.
58
+
59
+ ```python
60
+ with SageX3Requester(config) as client:
61
+ # Busca todos os clientes (cuidado com grandes volumes de dados!)
62
+ todos_clientes = client.request("CLIENTES", "BPC") \
63
+ .get_resources(fetch_all=True)
64
+
65
+ print(f"Total encontrado: {len(todos_clientes)}")
66
+ ```
67
+
68
+ ### Contagem Total
69
+ Para saber quantos registos existem sem trazer os dados:
70
+
71
+ ```python
72
+ with SageX3Requester(config) as client:
73
+ query = client.request("CLIENTES", "BPC").count(True).execute()
74
+ total = query.get("count") # ou verificar estrutura de retorno
75
+ ```
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ internal_lib_48e662a6.egg-info/PKG-INFO
4
+ internal_lib_48e662a6.egg-info/SOURCES.txt
5
+ internal_lib_48e662a6.egg-info/dependency_links.txt
6
+ internal_lib_48e662a6.egg-info/requires.txt
7
+ internal_lib_48e662a6.egg-info/top_level.txt
8
+ sage_x3_requests/__init__.py
9
+ tests/test_count_param.py
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "internal-lib-48e662a6"
7
+ version = "0.1.0a5"
8
+ description = "Internal utility package"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "Topic :: Software Development :: Libraries :: Python Modules",
15
+ ]
16
+ dependencies = [
17
+ "httpx",
18
+ "pydantic"
19
+ ]
20
+
21
+ [project.urls]
22
+ "Homepage" = "https://github.com/seu-usuario/sage_x3_requests"
@@ -0,0 +1,582 @@
1
+ import httpx
2
+ import asyncio
3
+ from typing import Optional, Dict, Any, List, Union
4
+ from pydantic import BaseModel, SecretStr
5
+ from urllib.parse import quote
6
+
7
+
8
+ class SageX3Config(BaseModel):
9
+ base_url: str
10
+ username: str
11
+ password: SecretStr
12
+ folder: str
13
+ api_version: str = "api1"
14
+ app: str = "x3"
15
+ env_prefix: str = "erp"
16
+ language: str = "PT"
17
+
18
+
19
+ class SageX3QueryBuilder:
20
+ """Builder para construir queries ao Sage X3"""
21
+
22
+ def __init__(self, client: 'SageX3Requester', endpoint: str, representation: str):
23
+ self.client = client
24
+ self.endpoint = endpoint
25
+ self.representation = representation
26
+ self._filter: Optional[str] = None
27
+ self._top: Optional[int] = None
28
+ self._skip: Optional[int] = None
29
+ self._items_per_page: Optional[int] = None
30
+ self._order_by: List[tuple] = []
31
+ self._count: bool = False
32
+ self._fields: List[str] = []
33
+
34
+ def filter(self, condition: str) -> 'SageX3QueryBuilder':
35
+ """Adiciona filtro WHERE"""
36
+ self._filter = condition
37
+ return self
38
+
39
+ def top(self, n: int) -> 'SageX3QueryBuilder':
40
+ """Limita número de resultados"""
41
+ self._top = n
42
+ return self
43
+
44
+ def skip(self, n: int) -> 'SageX3QueryBuilder':
45
+ """Salta N resultados (paginação)"""
46
+ self._skip = n
47
+ return self
48
+
49
+ def items_per_page(self, n: int) -> 'SageX3QueryBuilder':
50
+ """Define items por página"""
51
+ self._items_per_page = n
52
+ return self
53
+
54
+ def order_by(self, field: str, direction: str = "asc") -> 'SageX3QueryBuilder':
55
+ """Adiciona ordenação"""
56
+ self._order_by.append((field, direction))
57
+ return self
58
+
59
+ def count(self, enabled: bool = True) -> 'SageX3QueryBuilder':
60
+ """Retorna apenas contagem"""
61
+ self._count = enabled
62
+ return self
63
+
64
+ def select(self, *fields: str) -> 'SageX3QueryBuilder':
65
+ """Seleciona campos específicos"""
66
+ self._fields.extend(fields)
67
+ return self
68
+
69
+ def _build_url(self) -> str:
70
+ """Constrói URL completo"""
71
+ config = self.client.config
72
+ base = f"{config.base_url}/{config.api_version}/{config.app}/{config.env_prefix}"
73
+ url = f"{base}/{config.folder}/{self.endpoint}"
74
+ return url
75
+
76
+ def _build_params(self) -> Dict[str, Any]:
77
+ """Constrói parâmetros da query"""
78
+ params = {
79
+ "representation": f"{self.representation}.$query"
80
+ }
81
+
82
+ if self._filter:
83
+ params["where"] = self._filter
84
+
85
+ if self._top:
86
+ params["$top"] = self._top
87
+
88
+ if self._skip:
89
+ params["$skip"] = self._skip
90
+
91
+ if self._items_per_page:
92
+ params["$itemsPerPage"] = self._items_per_page
93
+
94
+ if self._order_by:
95
+ order_parts = [f"{field} {direction}" for field, direction in self._order_by]
96
+ params["$orderby"] = ", ".join(order_parts)
97
+
98
+ if self._count is not None and self._count is not False:
99
+ if isinstance(self._count, bool):
100
+ if self._count:
101
+ params["count"] = 1
102
+ else:
103
+ params["count"] = self._count
104
+
105
+ if self._fields:
106
+ params["$select"] = ",".join(self._fields)
107
+
108
+ return params
109
+
110
+ def execute(self) -> Dict[str, Any]:
111
+ """Executa a query e retorna resposta completa"""
112
+ url = self._build_url()
113
+ params = self._build_params()
114
+
115
+ return self.client._execute_request(url, params)
116
+
117
+ def execute_raw(self) -> httpx.Response:
118
+ """Executa e retorna resposta raw"""
119
+ url = self._build_url()
120
+ params = self._build_params()
121
+
122
+ return self.client._execute_request_raw(url, params)
123
+
124
+ def get_resources(self, limit: Optional[int] = None, fetch_all: bool = False, page_size: int = 100) -> List[Dict[str, Any]]:
125
+ """
126
+ Executa a query e retorna a lista de recursos.
127
+
128
+ Args:
129
+ limit: Se definido, limita o número de resultados (usa .count()).
130
+ fetch_all: Se True, busca TODOS os registos paginando automaticamente (ignora limit se este não for suportado como cap total).
131
+ page_size: Usado apenas se fetch_all=True, define tamanho da página.
132
+ """
133
+ if limit is not None:
134
+ self.count(limit)
135
+
136
+ if not fetch_all:
137
+ result = self.execute()
138
+ return result.get("$resources", [])
139
+
140
+ # Lógica de fetch_all (paginação)
141
+ all_resources = []
142
+ page_count = 0
143
+ max_pages = None # Sem limite de páginas por padrão
144
+
145
+ # Guardar configuração original
146
+ original_items_per_page = self._items_per_page
147
+ self._items_per_page = page_size
148
+
149
+ try:
150
+ # Buscar primeira página
151
+ result = self.execute()
152
+ resources = result.get("$resources", [])
153
+
154
+ if resources:
155
+ all_resources.extend(resources)
156
+ page_count += 1
157
+
158
+ # Seguir links $next enquanto existirem
159
+ while "$links" in result and "$next" in result["$links"]:
160
+ # Verificar se já atingimos o limit (se definido e se a API não o fez)
161
+ if limit and len(all_resources) >= limit:
162
+ all_resources = all_resources[:limit]
163
+ break
164
+
165
+ # Obter URL da próxima página
166
+ next_url = result["$links"]["$next"]["$url"]
167
+
168
+ # Fazer pedido direto ao URL fornecido
169
+ if not self.client._client:
170
+ raise RuntimeError("Client não inicializado")
171
+
172
+ response = self.client._client.get(next_url)
173
+ response.raise_for_status()
174
+ result = response.json()
175
+
176
+ resources = result.get("$resources", [])
177
+ if not resources:
178
+ break
179
+
180
+ all_resources.extend(resources)
181
+ page_count += 1
182
+
183
+ if limit and len(all_resources) > limit:
184
+ all_resources = all_resources[:limit]
185
+
186
+ return all_resources
187
+
188
+ finally:
189
+ # Restaurar configuração original
190
+ self._items_per_page = original_items_per_page
191
+
192
+ def get_first(self) -> Optional[Dict[str, Any]]:
193
+ """Executa a query e retorna apenas o primeiro recurso"""
194
+ resources = self.get_resources()
195
+ return resources[0] if resources else None
196
+
197
+ def get_count(self) -> int:
198
+ """Retorna o número de itens (usa $itemsPerPage ou conta $resources)"""
199
+ result = self.execute()
200
+ return result.get("$itemsPerPage", len(result.get("$resources", [])))
201
+
202
+
203
+
204
+ class SageX3Requester:
205
+ """Cliente para fazer pedidos ao Sage X3 (síncrono)"""
206
+
207
+ def __init__(self, config: SageX3Config):
208
+ self.config = config
209
+ self._client: Optional[httpx.Client] = None
210
+
211
+ def __enter__(self):
212
+ """Context manager entry"""
213
+ self._client = httpx.Client(
214
+ auth=(self.config.username, self.config.password.get_secret_value()),
215
+ headers={
216
+ "Accept": "application/json",
217
+ "Accept-Language": self.config.language
218
+ },
219
+ timeout=30.0
220
+ )
221
+ return self
222
+
223
+ def __exit__(self, exc_type, exc_val, exc_tb):
224
+ """Context manager exit"""
225
+ if self._client:
226
+ self._client.close()
227
+
228
+ def request(self, endpoint: str, representation: str) -> SageX3QueryBuilder:
229
+ """Inicia construção de query"""
230
+ return SageX3QueryBuilder(self, endpoint, representation)
231
+
232
+ def get_details(self, endpoint: str, representation: str, resource_id: str) -> Dict[str, Any]:
233
+ """
234
+ Obtém detalhes completos de um recurso específico usando a faceta $details.
235
+
236
+ Args:
237
+ endpoint: Nome do endpoint (ex: "YEQEV")
238
+ representation: Nome da representação (ex: "YEQEV2")
239
+ resource_id: ID do recurso (ex: "TRA000000002")
240
+
241
+ Returns:
242
+ Dicionário com todos os detalhes do recurso
243
+
244
+ Example:
245
+ details = client.get_details("YEQEV", "YEQEV2", "TRA000000002")
246
+ print(details["YEQPEVDES"])
247
+ """
248
+ config = self.config
249
+ base = f"{config.base_url}/{config.api_version}/{config.app}/{config.env_prefix}"
250
+ url = f"{base}/{config.folder}/{endpoint}('{resource_id}')"
251
+
252
+ params = {
253
+ "representation": f"{representation}.$details"
254
+ }
255
+
256
+ return self._execute_request(url, params)
257
+
258
+ def _build_url_with_params(self, url: str, params: Dict[str, Any]) -> str:
259
+ """
260
+ Constrói URL com parâmetros sem fazer encoding de aspas simples.
261
+ O Sage X3 não aceita aspas encoded (%27).
262
+ """
263
+ if not params:
264
+ return url
265
+
266
+ param_parts = []
267
+ for key, value in params.items():
268
+ # Converter valor para string
269
+ str_value = str(value)
270
+
271
+ # Fazer encoding de espaços e caracteres especiais, mas preservar aspas simples
272
+ encoded_value = quote(str_value, safe="'")
273
+ param_parts.append(f"{key}={encoded_value}")
274
+
275
+ query_string = "&".join(param_parts)
276
+ return f"{url}?{query_string}"
277
+
278
+ def _execute_request(self, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
279
+ """Executa pedido HTTP e retorna JSON"""
280
+ if not self._client:
281
+ raise RuntimeError("Client não inicializado. Use 'with SageX3Requester(config) as client:'")
282
+
283
+ # Construir URL manualmente para evitar encoding de aspas simples
284
+ url_with_params = self._build_url_with_params(url, params)
285
+ response = self._client.get(url_with_params)
286
+ response.raise_for_status()
287
+ return response.json()
288
+
289
+ def _execute_request_raw(self, url: str, params: Dict[str, Any]) -> httpx.Response:
290
+ """Executa pedido HTTP e retorna resposta raw"""
291
+ if not self._client:
292
+ raise RuntimeError("Client não inicializado. Use 'with SageX3Requester(config) as client:'")
293
+
294
+ # Construir URL manualmente para evitar encoding de aspas simples
295
+ url_with_params = self._build_url_with_params(url, params)
296
+ response = self._client.get(url_with_params)
297
+ response.raise_for_status()
298
+ return response
299
+
300
+
301
+ class AsyncSageX3QueryBuilder:
302
+ """Builder assíncrono para construir queries ao Sage X3"""
303
+
304
+ def __init__(self, client: 'AsyncSageX3Requester', endpoint: str, representation: str):
305
+ self.client = client
306
+ self.endpoint = endpoint
307
+ self.representation = representation
308
+ self._filter: Optional[str] = None
309
+ self._top: Optional[int] = None
310
+ self._skip: Optional[int] = None
311
+ self._items_per_page: Optional[int] = None
312
+ self._order_by: List[tuple] = []
313
+ self._count: bool = False
314
+ self._fields: List[str] = []
315
+
316
+ def filter(self, condition: str) -> 'AsyncSageX3QueryBuilder':
317
+ """Adiciona filtro WHERE"""
318
+ self._filter = condition
319
+ return self
320
+
321
+ def top(self, n: int) -> 'AsyncSageX3QueryBuilder':
322
+ """Limita número de resultados"""
323
+ self._top = n
324
+ return self
325
+
326
+ def skip(self, n: int) -> 'AsyncSageX3QueryBuilder':
327
+ """Salta N resultados (paginação)"""
328
+ self._skip = n
329
+ return self
330
+
331
+ def items_per_page(self, n: int) -> 'AsyncSageX3QueryBuilder':
332
+ """Define items por página"""
333
+ self._items_per_page = n
334
+ return self
335
+
336
+ def order_by(self, field: str, direction: str = "asc") -> 'AsyncSageX3QueryBuilder':
337
+ """Adiciona ordenação"""
338
+ self._order_by.append((field, direction))
339
+ return self
340
+
341
+ def count(self, enabled: bool = True) -> 'AsyncSageX3QueryBuilder':
342
+ """Retorna apenas contagem"""
343
+ self._count = enabled
344
+ return self
345
+
346
+ def select(self, *fields: str) -> 'AsyncSageX3QueryBuilder':
347
+ """Seleciona campos específicos"""
348
+ self._fields.extend(fields)
349
+ return self
350
+
351
+ def _build_url(self) -> str:
352
+ """Constrói URL completo"""
353
+ config = self.client.config
354
+ base = f"{config.base_url}/{config.api_version}/{config.app}/{config.env_prefix}"
355
+ url = f"{base}/{config.folder}/{self.endpoint}"
356
+ return url
357
+
358
+ def _build_params(self) -> Dict[str, Any]:
359
+ """Constrói parâmetros da query"""
360
+ params = {
361
+ "representation": f"{self.representation}.$query"
362
+ }
363
+
364
+ if self._filter:
365
+ params["where"] = self._filter
366
+
367
+ if self._top:
368
+ params["$top"] = self._top
369
+
370
+ if self._skip:
371
+ params["$skip"] = self._skip
372
+
373
+ if self._items_per_page:
374
+ params["$itemsPerPage"] = self._items_per_page
375
+
376
+ if self._order_by:
377
+ order_parts = [f"{field} {direction}" for field, direction in self._order_by]
378
+ params["$orderby"] = ", ".join(order_parts)
379
+
380
+ if self._count is not None and self._count is not False:
381
+ if isinstance(self._count, bool):
382
+ if self._count:
383
+ params["count"] = 1
384
+ else:
385
+ params["count"] = self._count
386
+
387
+ if self._fields:
388
+ params["$select"] = ",".join(self._fields)
389
+
390
+ return params
391
+
392
+ async def execute(self) -> Dict[str, Any]:
393
+ """Executa a query e retorna resposta completa"""
394
+ url = self._build_url()
395
+ params = self._build_params()
396
+
397
+ return await self.client._execute_request(url, params)
398
+
399
+ async def get_resources(self, limit: Optional[int] = None, fetch_all: bool = False, page_size: int = 100) -> List[Dict[str, Any]]:
400
+ """
401
+ Executa a query e retorna a lista de recursos.
402
+
403
+ Args:
404
+ limit: Se definido, limita o número de resultados (usa .count()).
405
+ fetch_all: Se True, busca TODOS os registos paginando automaticamente (ignora limit se este não for suportado como cap total).
406
+ page_size: Usado apenas se fetch_all=True, define tamanho da página.
407
+ """
408
+ if limit is not None:
409
+ self.count(limit)
410
+
411
+ if not fetch_all:
412
+ result = await self.execute()
413
+ return result.get("$resources", [])
414
+
415
+ # Lógica de fetch_all (paginação)
416
+ all_resources = []
417
+ page_count = 0
418
+ max_pages = None # Sem limite de páginas por padrão
419
+
420
+ # Guardar configuração original
421
+ original_items_per_page = self._items_per_page
422
+ self._items_per_page = page_size
423
+
424
+ try:
425
+ # Buscar primeira página
426
+ result = await self.execute()
427
+ resources = result.get("$resources", [])
428
+
429
+ if resources:
430
+ all_resources.extend(resources)
431
+ page_count += 1
432
+
433
+ # Seguir links $next enquanto existirem
434
+ while "$links" in result and "$next" in result["$links"]:
435
+ # Verificar se já atingimos o limit (se definido e se a API não o fez)
436
+ if limit and len(all_resources) >= limit:
437
+ all_resources = all_resources[:limit]
438
+ break
439
+
440
+ # Obter URL da próxima página
441
+ next_url = result["$links"]["$next"]["$url"]
442
+
443
+ # Fazer pedido direto ao URL fornecido
444
+ if not self.client._client:
445
+ raise RuntimeError("Client não inicializado")
446
+
447
+ response = await self.client._client.get(next_url)
448
+ response.raise_for_status()
449
+ result = response.json()
450
+
451
+ resources = result.get("$resources", [])
452
+ if not resources:
453
+ break
454
+
455
+ all_resources.extend(resources)
456
+ page_count += 1
457
+
458
+ if limit and len(all_resources) > limit:
459
+ all_resources = all_resources[:limit]
460
+
461
+ return all_resources
462
+
463
+ finally:
464
+ # Restaurar configuração original
465
+ self._items_per_page = original_items_per_page
466
+
467
+ async def get_first(self) -> Optional[Dict[str, Any]]:
468
+ """Executa a query e retorna apenas o primeiro recurso"""
469
+ resources = await self.get_resources()
470
+ return resources[0] if resources else None
471
+
472
+ async def get_count(self) -> int:
473
+ """Retorna o número de itens"""
474
+ result = await self.execute()
475
+ return result.get("$itemsPerPage", len(result.get("$resources", [])))
476
+
477
+
478
+ class AsyncSageX3Requester:
479
+ """Cliente assíncrono para fazer pedidos ao Sage X3 (até 10x mais rápido) 🚀"""
480
+
481
+ def __init__(self, config: SageX3Config):
482
+ self.config = config
483
+ self._client: Optional[httpx.AsyncClient] = None
484
+
485
+ async def __aenter__(self):
486
+ """Context manager entry"""
487
+ self._client = httpx.AsyncClient(
488
+ auth=(self.config.username, self.config.password.get_secret_value()),
489
+ headers={
490
+ "Accept": "application/json",
491
+ "Accept-Language": self.config.language
492
+ },
493
+ timeout=30.0,
494
+ limits=httpx.Limits(max_keepalive_connections=20, max_connections=100)
495
+ )
496
+ return self
497
+
498
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
499
+ """Context manager exit"""
500
+ if self._client:
501
+ await self._client.aclose()
502
+
503
+ def request(self, endpoint: str, representation: str) -> AsyncSageX3QueryBuilder:
504
+ """Inicia construção de query (mesma sintaxe que a versão sync!)"""
505
+ return AsyncSageX3QueryBuilder(self, endpoint, representation)
506
+
507
+ def _build_url_with_params(self, url: str, params: Dict[str, Any]) -> str:
508
+ """Constrói URL com parâmetros preservando aspas simples"""
509
+ if not params:
510
+ return url
511
+
512
+ param_parts = []
513
+ for key, value in params.items():
514
+ str_value = str(value)
515
+ encoded_value = quote(str_value, safe="'")
516
+ param_parts.append(f"{key}={encoded_value}")
517
+
518
+ query_string = "&".join(param_parts)
519
+ return f"{url}?{query_string}"
520
+
521
+ async def _execute_request(self, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
522
+ """Executa pedido HTTP assíncrono e retorna JSON"""
523
+ if not self._client:
524
+ raise RuntimeError("Client não inicializado")
525
+
526
+ url_with_params = self._build_url_with_params(url, params)
527
+ response = await self._client.get(url_with_params)
528
+ response.raise_for_status()
529
+ return response.json()
530
+
531
+ async def get_details(self, endpoint: str, representation: str, resource_id: str) -> Dict[str, Any]:
532
+ """
533
+ Obtém detalhes de um recurso específico (assíncrono).
534
+
535
+ Example:
536
+ details = await client.get_details("YEQEV", "YEQEV2", "TRA000000002")
537
+ """
538
+ if not self._client:
539
+ raise RuntimeError("Client não inicializado")
540
+
541
+ config = self.config
542
+ base = f"{config.base_url}/{config.api_version}/{config.app}/{config.env_prefix}"
543
+ url = f"{base}/{config.folder}/{endpoint}('{resource_id}')"
544
+
545
+ params = {"representation": f"{representation}.$details"}
546
+ url_with_params = self._build_url_with_params(url, params)
547
+
548
+ response = await self._client.get(url_with_params)
549
+ response.raise_for_status()
550
+ return response.json()
551
+
552
+ async def get_multiple_details(
553
+ self,
554
+ endpoint: str,
555
+ representation: str,
556
+ resource_ids: List[str],
557
+ concurrent_requests: int = 10
558
+ ) -> List[Dict[str, Any]]:
559
+ """
560
+ Busca detalhes de múltiplos recursos em paralelo (MUITO RÁPIDO! 🚀).
561
+
562
+ Args:
563
+ endpoint: Nome do endpoint
564
+ representation: Nome da representação
565
+ resource_ids: Lista de IDs para buscar
566
+ concurrent_requests: Número de pedidos simultâneos (padrão: 10)
567
+
568
+ Returns:
569
+ Lista com detalhes de todos os recursos
570
+
571
+ Example:
572
+ ids = ["TRA000000001", "TRA000000002", "TRA000000003"]
573
+ details = await client.get_multiple_details("YEQEV", "YEQEV2", ids)
574
+ """
575
+ semaphore = asyncio.Semaphore(concurrent_requests)
576
+
577
+ async def fetch_one(resource_id: str):
578
+ async with semaphore:
579
+ return await self.get_details(endpoint, representation, resource_id)
580
+
581
+ tasks = [fetch_one(rid) for rid in resource_ids]
582
+ return await asyncio.gather(*tasks)
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,44 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock
3
+ from sage_x3_requests import SageX3Requester, SageX3Config, SageX3QueryBuilder
4
+ from pydantic import SecretStr
5
+
6
+ class TestSageX3CountParam(unittest.TestCase):
7
+ def setUp(self):
8
+ self.config = SageX3Config(
9
+ base_url="http://test.com",
10
+ username="user",
11
+ password="password",
12
+ folder="TEST"
13
+ )
14
+ self.client = SageX3Requester(self.config)
15
+ self.builder = SageX3QueryBuilder(self.client, "TEST_ENDPOINT", "TEST_REP")
16
+
17
+ def test_count_as_bool_true(self):
18
+ self.builder.count(True)
19
+ params = self.builder._build_params()
20
+ self.assertEqual(params.get("count"), 1)
21
+
22
+ def test_count_as_bool_false(self):
23
+ self.builder.count(False)
24
+ params = self.builder._build_params()
25
+ self.assertIsNone(params.get("count"))
26
+
27
+ def test_count_as_int(self):
28
+ self.builder.count(5)
29
+ params = self.builder._build_params()
30
+ self.assertEqual(params.get("count"), 5)
31
+
32
+ def test_top_still_works(self):
33
+ self.builder.top(10)
34
+ params = self.builder._build_params()
35
+ self.assertEqual(params.get("$top"), 10)
36
+
37
+ def test_both_top_and_count(self):
38
+ self.builder.top(10).count(5)
39
+ params = self.builder._build_params()
40
+ self.assertEqual(params.get("$top"), 10)
41
+ self.assertEqual(params.get("count"), 5)
42
+
43
+ if __name__ == '__main__':
44
+ unittest.main()