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.
- internal_lib_48e662a6-0.1.0a5/PKG-INFO +75 -0
- internal_lib_48e662a6-0.1.0a5/README.md +62 -0
- internal_lib_48e662a6-0.1.0a5/internal_lib_48e662a6.egg-info/PKG-INFO +75 -0
- internal_lib_48e662a6-0.1.0a5/internal_lib_48e662a6.egg-info/SOURCES.txt +9 -0
- internal_lib_48e662a6-0.1.0a5/internal_lib_48e662a6.egg-info/dependency_links.txt +1 -0
- internal_lib_48e662a6-0.1.0a5/internal_lib_48e662a6.egg-info/requires.txt +2 -0
- internal_lib_48e662a6-0.1.0a5/internal_lib_48e662a6.egg-info/top_level.txt +1 -0
- internal_lib_48e662a6-0.1.0a5/pyproject.toml +22 -0
- internal_lib_48e662a6-0.1.0a5/sage_x3_requests/__init__.py +582 -0
- internal_lib_48e662a6-0.1.0a5/setup.cfg +4 -0
- internal_lib_48e662a6-0.1.0a5/tests/test_count_param.py +44 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sage_x3_requests
|
|
@@ -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,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()
|