konecty_sdk_python 2.1.5__tar.gz → 2.1.7__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.
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/client.py +73 -28
- konecty_sdk_python-2.1.7/KonectySdkPython/lib/exceptions.py +259 -0
- konecty_sdk_python-2.1.7/KonectySdkPython/lib/filters.py +291 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/auth.py +28 -1
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/PKG-INFO +1 -1
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/pyproject.toml +1 -1
- konecty_sdk_python-2.1.5/KonectySdkPython/lib/exceptions.py +0 -93
- konecty_sdk_python-2.1.5/KonectySdkPython/lib/filters.py +0 -155
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/.gitignore +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/__init__.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/cli/__init__.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/cli/apply.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/cli/backup.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/cli/pull.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/__init__.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/feature_types/__init__.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/feature_types/cross_module_query.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/feature_types/kpi.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/feature_types/query_json.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/file_manager.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/http.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/model.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/serialization.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/__init__.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/admin.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/aggregation.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/base.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/change_user.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/comments.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/export.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/files.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/notifications.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/pat.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/query.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/stream.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/subscriptions.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/settings.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/types.py +0 -0
- {konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/README.md +0 -0
|
@@ -52,6 +52,25 @@ KONECTY_UPDATE_IGNORE_FIELDS = [
|
|
|
52
52
|
KONECTY_CREATE_IGNORE_FIELDS = ["_updatedAt", "_createdAt", "_updatedBy", "_createdBy"]
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
def _find_query_params(options: KonectyFindParams, get_total: bool) -> Dict[str, str]:
|
|
56
|
+
"""Query string de /rest/data/{module}/find.
|
|
57
|
+
|
|
58
|
+
`getTotal=false` só vai para a rede quando a contagem é desligada: com o
|
|
59
|
+
padrão a query fica idêntica à de antes. Paridade com `KonectyClient.find` do
|
|
60
|
+
SDK TypeScript, que também só envia o desligamento, sempre no fim da query.
|
|
61
|
+
"""
|
|
62
|
+
params: Dict[str, str] = {}
|
|
63
|
+
for key, value in options.model_dump(exclude_none=True).items():
|
|
64
|
+
params[key] = (
|
|
65
|
+
json.dumps(value, default=json_serial)
|
|
66
|
+
if key != "fields"
|
|
67
|
+
else ",".join(value)
|
|
68
|
+
)
|
|
69
|
+
if get_total is False:
|
|
70
|
+
params["getTotal"] = "false"
|
|
71
|
+
return params
|
|
72
|
+
|
|
73
|
+
|
|
55
74
|
def get_first_dict(items: List[Any]) -> Optional[KonectyDict]:
|
|
56
75
|
"""Retorna o primeiro item de uma lista como dicionário ou None se estiver vazia."""
|
|
57
76
|
if not items:
|
|
@@ -67,11 +86,20 @@ async def _read_json_body(response: "aiohttp.ClientResponse") -> Optional[Dict[s
|
|
|
67
86
|
Lê o corpo como JSON sem explodir quando não é JSON (proxy, gateway, HTML).
|
|
68
87
|
|
|
69
88
|
Devolve ``None`` nesse caso, e o chamador cai no tratamento por status.
|
|
89
|
+
|
|
90
|
+
Também devolve ``None`` quando o corpo é JSON válido mas **não é objeto** —
|
|
91
|
+
uma string ou lista, que é o que alguns proxies devolvem. O ``cast`` não
|
|
92
|
+
verifica nada em runtime, então sem esta checagem o chamador fazia
|
|
93
|
+
``result.get(...)`` num ``str`` e recebia ``AttributeError`` no lugar do erro
|
|
94
|
+
de API.
|
|
70
95
|
"""
|
|
71
96
|
try:
|
|
72
|
-
|
|
97
|
+
body = await response.json(content_type=None)
|
|
73
98
|
except Exception: # noqa: BLE001 - corpo não-JSON é caso esperado aqui
|
|
74
99
|
return None
|
|
100
|
+
if not isinstance(body, dict):
|
|
101
|
+
return None
|
|
102
|
+
return cast(Dict[str, Any], body)
|
|
75
103
|
|
|
76
104
|
|
|
77
105
|
class KonectyClient:
|
|
@@ -439,6 +467,14 @@ class KonectyClient:
|
|
|
439
467
|
"""Get login method flags for the namespace (GET /api/auth/login-options)."""
|
|
440
468
|
return await self._auth.get_login_options()
|
|
441
469
|
|
|
470
|
+
async def info(self) -> Dict[str, Any]:
|
|
471
|
+
"""
|
|
472
|
+
Get the session user (GET /rest/auth/info), including ``user["redirectURL"]``.
|
|
473
|
+
|
|
474
|
+
Returns ``{"logged": False}`` without a token or when the server refuses the session.
|
|
475
|
+
"""
|
|
476
|
+
return await self._auth.info()
|
|
477
|
+
|
|
442
478
|
@property
|
|
443
479
|
def _pat(self) -> PatService:
|
|
444
480
|
if not hasattr(self, "_pat_service"):
|
|
@@ -655,14 +691,16 @@ class KonectyClient:
|
|
|
655
691
|
query_id, shared_with, is_public=is_public
|
|
656
692
|
)
|
|
657
693
|
|
|
658
|
-
async def find(
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
694
|
+
async def find(
|
|
695
|
+
self, module: str, options: KonectyFindParams, get_total: bool = True
|
|
696
|
+
) -> List[KonectyDict]:
|
|
697
|
+
"""Busca registros em GET /rest/data/{module}/find.
|
|
698
|
+
|
|
699
|
+
`get_total=False` pede ao servidor para não contar o total (a resposta vem
|
|
700
|
+
sem `total`), o que acelera listagens grandes quando o total não é
|
|
701
|
+
necessário. Com o padrão (`True`) nada é enviado e a URL é a mesma de antes.
|
|
702
|
+
"""
|
|
703
|
+
params = _find_query_params(options, get_total)
|
|
666
704
|
|
|
667
705
|
async with (
|
|
668
706
|
aiohttp.ClientSession() as session,
|
|
@@ -698,7 +736,6 @@ class KonectyClient:
|
|
|
698
736
|
if search:
|
|
699
737
|
params["search"] = search
|
|
700
738
|
|
|
701
|
-
print(f"params: {params}")
|
|
702
739
|
async with (
|
|
703
740
|
aiohttp.ClientSession() as session,
|
|
704
741
|
session.get(
|
|
@@ -707,24 +744,24 @@ class KonectyClient:
|
|
|
707
744
|
headers={"Authorization": self.headers["Authorization"]},
|
|
708
745
|
) as response,
|
|
709
746
|
):
|
|
710
|
-
|
|
711
|
-
result = await response
|
|
747
|
+
# Mesma ordem do `find`: corpo primeiro, status depois. Ver `find`.
|
|
748
|
+
result = await _read_json_body(response)
|
|
749
|
+
if result is None:
|
|
750
|
+
response.raise_for_status()
|
|
751
|
+
raise KonectyAPIError(f"{response.status} {response.reason}")
|
|
712
752
|
if not result.get("success", False):
|
|
713
753
|
errors = result.get("errors", [])
|
|
714
754
|
logger.error(errors)
|
|
715
|
-
|
|
755
|
+
raise_for_konecty_errors(errors)
|
|
756
|
+
response.raise_for_status()
|
|
716
757
|
data = result.get("data", [])
|
|
717
758
|
return cast(List[KonectyDict], data)
|
|
718
759
|
|
|
719
|
-
def find_sync(
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
json.dumps(value, default=json_serial)
|
|
725
|
-
if key != "fields"
|
|
726
|
-
else ",".join(value)
|
|
727
|
-
)
|
|
760
|
+
def find_sync(
|
|
761
|
+
self, module: str, options: KonectyFindParams, get_total: bool = True
|
|
762
|
+
) -> List[KonectyDict]:
|
|
763
|
+
"""Versão síncrona de find (mesma semântica de `get_total`)."""
|
|
764
|
+
params = _find_query_params(options, get_total)
|
|
728
765
|
|
|
729
766
|
import requests
|
|
730
767
|
|
|
@@ -767,12 +804,16 @@ class KonectyClient:
|
|
|
767
804
|
headers={"Authorization": self.headers["Authorization"]},
|
|
768
805
|
) as response,
|
|
769
806
|
):
|
|
770
|
-
|
|
771
|
-
result = await response
|
|
807
|
+
# Mesma ordem do `find`: corpo primeiro, status depois. Ver `find`.
|
|
808
|
+
result = await _read_json_body(response)
|
|
809
|
+
if result is None:
|
|
810
|
+
response.raise_for_status()
|
|
811
|
+
raise KonectyAPIError(f"{response.status} {response.reason}")
|
|
772
812
|
if not result.get("success", False):
|
|
773
813
|
errors = result.get("errors", [])
|
|
774
814
|
logger.error(errors)
|
|
775
|
-
|
|
815
|
+
raise_for_konecty_errors(errors)
|
|
816
|
+
response.raise_for_status()
|
|
776
817
|
data = result.get("data", [None])
|
|
777
818
|
return get_first_dict(data)
|
|
778
819
|
|
|
@@ -1005,12 +1046,16 @@ class KonectyClient:
|
|
|
1005
1046
|
headers={"Authorization": self.headers["Authorization"]},
|
|
1006
1047
|
) as response,
|
|
1007
1048
|
):
|
|
1008
|
-
|
|
1009
|
-
result = await response
|
|
1049
|
+
# Mesma ordem do `find`: corpo primeiro, status depois. Ver `find`.
|
|
1050
|
+
result = await _read_json_body(response)
|
|
1051
|
+
if result is None:
|
|
1052
|
+
response.raise_for_status()
|
|
1053
|
+
raise KonectyAPIError(f"{response.status} {response.reason}")
|
|
1010
1054
|
if not result.get("success", False):
|
|
1011
1055
|
errors = result.get("errors", [])
|
|
1012
1056
|
logger.error(errors)
|
|
1013
|
-
|
|
1057
|
+
raise_for_konecty_errors(errors)
|
|
1058
|
+
response.raise_for_status()
|
|
1014
1059
|
count = result.get("total", 0)
|
|
1015
1060
|
return count
|
|
1016
1061
|
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Exceptions for the Konecty SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class KonectyError(Exception):
|
|
7
|
+
"""Base exception for Konecty errors."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class KonectyAPIError(KonectyError):
|
|
13
|
+
"""Raised when the API returns success=false or a non-2xx status."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class KonectyGoogleSessionError(KonectyAPIError):
|
|
19
|
+
"""
|
|
20
|
+
Raised by exchange_google_code. Carries the machine-readable ``code`` from
|
|
21
|
+
``errors[0].code`` so callers can branch or translate without parsing the
|
|
22
|
+
message; ``failed`` covers unreadable or unrecognised bodies.
|
|
23
|
+
|
|
24
|
+
Subclasses KonectyAPIError so existing ``except KonectyAPIError`` keeps working.
|
|
25
|
+
|
|
26
|
+
Mirrors KonectyGoogleSessionError in the TypeScript SDK — keep both in sync.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, code: str, message: str) -> None:
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.code = code
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
#: Código devolvido pelo Konecty quando a requisição pede ordenação arbitrária
|
|
35
|
+
#: acima do teto de página. Espelha ``SORT_ABOVE_MAX_PAGE_SIZE`` no SDK
|
|
36
|
+
#: TypeScript — manter os dois em sincronia.
|
|
37
|
+
SORT_ABOVE_MAX_PAGE_SIZE = "SORT_ABOVE_MAX_PAGE_SIZE"
|
|
38
|
+
|
|
39
|
+
#: Teto do servidor. Informativo: quem decide é a API, este valor só evita adivinhação.
|
|
40
|
+
MAX_SORTED_PAGE_SIZE = 1000
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class KonectySortLimitError(KonectyAPIError):
|
|
44
|
+
"""
|
|
45
|
+
Raised when the request asks for arbitrary ordering above the page cap.
|
|
46
|
+
|
|
47
|
+
Acima de ``MAX_SORTED_PAGE_SIZE`` registros — e no caso sem limite
|
|
48
|
+
(``limit=-1``) — o ``$sort`` teria de terminar antes de o primeiro documento
|
|
49
|
+
sair do cursor, então a API recusa com HTTP 400 em vez de trocar a ordenação
|
|
50
|
+
em silêncio, como fazia antes.
|
|
51
|
+
|
|
52
|
+
Carrega o ``code`` legível por máquina para o chamador ramificar sem
|
|
53
|
+
inspecionar a mensagem, como ``KonectyGoogleSessionError``. Subclasse de
|
|
54
|
+
``KonectyAPIError``, então ``except KonectyAPIError`` existente continua
|
|
55
|
+
pegando.
|
|
56
|
+
|
|
57
|
+
A saída é ordenar por ``_id`` (ascendente ou descendente) e paginar por faixa
|
|
58
|
+
de ``_id``, ou pedir no máximo ``MAX_SORTED_PAGE_SIZE`` registros.
|
|
59
|
+
|
|
60
|
+
Espelha ``KonectySortLimitError`` no SDK TypeScript — manter os dois em sincronia.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, message: str) -> None:
|
|
64
|
+
super().__init__(message)
|
|
65
|
+
self.code = SORT_ABOVE_MAX_PAGE_SIZE
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
#: Códigos do operador de filtro ``within_radius``. Espelham as constantes de
|
|
69
|
+
#: mesmo nome no SDK TypeScript (``src/sdk/filters/withinRadius.ts``) e no
|
|
70
|
+
#: servidor (``src/imports/data/filters/withinRadius.ts`` no repo Konecty).
|
|
71
|
+
#:
|
|
72
|
+
#: Recusa de forma ou de faixa do valor: ``lat``/``lng`` (ou ``record``) ou
|
|
73
|
+
#: ``radius`` ausente, as duas formas juntas, chave desconhecida, coordenada fora
|
|
74
|
+
#: da faixa, string numérica no lugar de número, raio não positivo ou acima do
|
|
75
|
+
#: teto do servidor.
|
|
76
|
+
#: Nome do operador, usado nas mensagens default. Espelha `WITHIN_RADIUS` no SDK TS.
|
|
77
|
+
WITHIN_RADIUS_OPERATOR = "within_radius"
|
|
78
|
+
|
|
79
|
+
WITHIN_RADIUS_INVALID_VALUE = "WITHIN_RADIUS_INVALID_VALUE"
|
|
80
|
+
|
|
81
|
+
#: O centro por referência a registro não pôde ser resolvido. Um código só para
|
|
82
|
+
#: os três casos (registro inexistente, ilegível, ou sem geolocalização) é
|
|
83
|
+
#: decisão do servidor: distinguí-los na resposta transformaria o filtro num
|
|
84
|
+
#: oráculo de localização para quem não pode ler o registro.
|
|
85
|
+
WITHIN_RADIUS_CENTER_UNRESOLVED = "WITHIN_RADIUS_CENTER_UNRESOLVED"
|
|
86
|
+
|
|
87
|
+
#: A cadeia de centros por referência passou do teto de profundidade do servidor.
|
|
88
|
+
#: Acontece quando um centro aponta para um registro cujo filtro de leitura tem, ele
|
|
89
|
+
#: próprio, um `within_radius` com centro por referência — a resolução é recursiva e o
|
|
90
|
+
#: servidor corta. Não é erro do valor enviado: é configuração de metadado do documento
|
|
91
|
+
#: alvo. Espelha `WITHIN_RADIUS_CENTER_DEPTH_EXCEEDED` no SDK TS.
|
|
92
|
+
WITHIN_RADIUS_CENTER_DEPTH_EXCEEDED = "WITHIN_RADIUS_CENTER_DEPTH_EXCEEDED"
|
|
93
|
+
|
|
94
|
+
#: A requisição pediu mais centros por referência do que o servidor resolve de uma vez.
|
|
95
|
+
#: O SDK **não** duplica o teto: quem decide é o servidor, e um número copiado aqui
|
|
96
|
+
#: passaria a mentir assim que o backend mudasse. Espelha `WITHIN_RADIUS_TOO_MANY_CENTERS`
|
|
97
|
+
#: no SDK TS.
|
|
98
|
+
WITHIN_RADIUS_TOO_MANY_CENTERS = "WITHIN_RADIUS_TOO_MANY_CENTERS"
|
|
99
|
+
|
|
100
|
+
#: Recusa de ordenação por ``_distance``: o filtro não tem exatamente um
|
|
101
|
+
#: ``within_radius`` no caminho AND, ou o usuário não pode ler (ou só lê sob
|
|
102
|
+
#: condição) o campo ``address`` do centro. O servidor responde HTTP 400; a mensagem
|
|
103
|
+
#: diz qual dos dois casos é. Espelha ``DISTANCE_SORT_UNAVAILABLE`` no SDK TS.
|
|
104
|
+
DISTANCE_SORT_UNAVAILABLE = "DISTANCE_SORT_UNAVAILABLE"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class KonectyWithinRadiusValueError(KonectyAPIError):
|
|
108
|
+
"""
|
|
109
|
+
Raised when the server refuses the shape or range of a ``within_radius`` value.
|
|
110
|
+
|
|
111
|
+
Carrega o ``code`` legível por máquina e preserva a mensagem do servidor, que
|
|
112
|
+
nomeia o termo e a chave exata que falhou. Subclasse de ``KonectyAPIError``,
|
|
113
|
+
então ``except KonectyAPIError`` existente continua pegando.
|
|
114
|
+
|
|
115
|
+
O SDK **não** duplica a validação de faixa nem o teto de raio: quem decide é
|
|
116
|
+
o servidor, e um teto copiado no cliente passa a mentir assim que o backend
|
|
117
|
+
muda. Mesma postura do ``SORT_ABOVE_MAX_PAGE_SIZE``.
|
|
118
|
+
|
|
119
|
+
Espelha ``KonectyWithinRadiusValueError`` no SDK TypeScript — manter os dois
|
|
120
|
+
em sincronia.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
#: Default idêntico ao do SDK TS (`message ?? \`Invalid value for operator ${WITHIN_RADIUS}\``
|
|
124
|
+
#: em `src/sdk/filters/withinRadius.ts`), para que um erro sem `message` produza a MESMA
|
|
125
|
+
#: frase nos dois — antes o Python levantava com a string literal "None".
|
|
126
|
+
def __init__(self, message: Optional[str] = None) -> None:
|
|
127
|
+
super().__init__(
|
|
128
|
+
message or f"Invalid value for operator {WITHIN_RADIUS_OPERATOR}"
|
|
129
|
+
)
|
|
130
|
+
self.code = WITHIN_RADIUS_INVALID_VALUE
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class KonectyWithinRadiusCenterError(KonectyAPIError):
|
|
134
|
+
"""
|
|
135
|
+
Raised when a ``within_radius`` center given as a record reference could not
|
|
136
|
+
be resolved — the record does not exist, is not readable, or has no geolocation.
|
|
137
|
+
|
|
138
|
+
Espelha ``KonectyWithinRadiusCenterError`` no SDK TypeScript — manter os dois
|
|
139
|
+
em sincronia.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
#: Mesmo default do SDK TS — ver `KonectyWithinRadiusValueError` acima.
|
|
143
|
+
def __init__(self, message: Optional[str] = None) -> None:
|
|
144
|
+
super().__init__(
|
|
145
|
+
message
|
|
146
|
+
or f"Could not resolve the center record for operator {WITHIN_RADIUS_OPERATOR}"
|
|
147
|
+
)
|
|
148
|
+
self.code = WITHIN_RADIUS_CENTER_UNRESOLVED
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class KonectyWithinRadiusCenterDepthError(KonectyWithinRadiusCenterError):
|
|
152
|
+
"""
|
|
153
|
+
Raised when the chain of center-by-reference resolutions exceeds the server depth cap.
|
|
154
|
+
|
|
155
|
+
**Herda de ``KonectyWithinRadiusCenterError`` de propósito**, como no SDK TS: é uma
|
|
156
|
+
falha de resolução de centro, e a própria mensagem do servidor começa com "Could not
|
|
157
|
+
resolve the center record". Quem escreveu ``except KonectyWithinRadiusCenterError``
|
|
158
|
+
antes deste código existir continua pegando.
|
|
159
|
+
|
|
160
|
+
Espelha ``KonectyWithinRadiusCenterDepthError`` no SDK TypeScript.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(self, message: Optional[str] = None) -> None:
|
|
164
|
+
super().__init__(
|
|
165
|
+
message
|
|
166
|
+
or f"Center hydration exceeded the maximum depth for operator {WITHIN_RADIUS_OPERATOR}"
|
|
167
|
+
)
|
|
168
|
+
self.code = WITHIN_RADIUS_CENTER_DEPTH_EXCEEDED
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class KonectyWithinRadiusTooManyCentersError(KonectyAPIError):
|
|
172
|
+
"""
|
|
173
|
+
Raised when the request asks for more center records than the server resolves at once.
|
|
174
|
+
|
|
175
|
+
**NÃO** herda de ``KonectyWithinRadiusCenterError``: nenhum centro específico falhou —
|
|
176
|
+
a requisição inteira foi recusada antes de qualquer leitura. Tratar cota como falha de
|
|
177
|
+
centro levaria o chamador a procurar um registro culpado que não existe.
|
|
178
|
+
|
|
179
|
+
Espelha ``KonectyWithinRadiusTooManyCentersError`` no SDK TypeScript.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, message: Optional[str] = None) -> None:
|
|
183
|
+
super().__init__(
|
|
184
|
+
message
|
|
185
|
+
or f"Too many center records requested for operator {WITHIN_RADIUS_OPERATOR}"
|
|
186
|
+
)
|
|
187
|
+
self.code = WITHIN_RADIUS_TOO_MANY_CENTERS
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class KonectyDistanceSortUnavailableError(KonectyAPIError):
|
|
191
|
+
"""
|
|
192
|
+
Raised when the server refuses a sort by ``_distance``.
|
|
193
|
+
|
|
194
|
+
**NÃO** herda de ``KonectySortLimitError``: não é teto de página, e a saída não é
|
|
195
|
+
ordenar por ``_id`` — é acertar o filtro (um ``within_radius`` só, no caminho AND)
|
|
196
|
+
ou a permissão de leitura do campo ``address``.
|
|
197
|
+
|
|
198
|
+
Espelha ``KonectyDistanceSortUnavailableError`` no SDK TypeScript.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
#: Mesmo default do SDK TS (`src/sdk/filters/withinRadius.ts`).
|
|
202
|
+
def __init__(self, message: Optional[str] = None) -> None:
|
|
203
|
+
super().__init__(
|
|
204
|
+
message or "Sorting by _distance is not available for this query"
|
|
205
|
+
)
|
|
206
|
+
self.code = DISTANCE_SORT_UNAVAILABLE
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
#: Códigos que o SDK promove a exceção própria. A tabela existe para que um
|
|
210
|
+
#: código novo seja UMA linha em vez de mais um ``if`` no meio do fluxo — e para
|
|
211
|
+
#: que a lista de códigos suportados seja legível de uma vez, ao lado da lista
|
|
212
|
+
#: equivalente no SDK TypeScript (``ERROR_BY_CODE`` em ``src/sdk/errors.ts``).
|
|
213
|
+
#:
|
|
214
|
+
#: Todo código ausente daqui continua virando ``KonectyAPIError`` genérico.
|
|
215
|
+
_ERROR_BY_CODE = {
|
|
216
|
+
SORT_ABOVE_MAX_PAGE_SIZE: KonectySortLimitError,
|
|
217
|
+
WITHIN_RADIUS_INVALID_VALUE: KonectyWithinRadiusValueError,
|
|
218
|
+
WITHIN_RADIUS_CENTER_UNRESOLVED: KonectyWithinRadiusCenterError,
|
|
219
|
+
WITHIN_RADIUS_CENTER_DEPTH_EXCEEDED: KonectyWithinRadiusCenterDepthError,
|
|
220
|
+
WITHIN_RADIUS_TOO_MANY_CENTERS: KonectyWithinRadiusTooManyCentersError,
|
|
221
|
+
DISTANCE_SORT_UNAVAILABLE: KonectyDistanceSortUnavailableError,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def raise_for_konecty_errors(errors: object) -> None:
|
|
226
|
+
"""
|
|
227
|
+
Lança a exceção mais específica que a lista de ``errors`` da API permitir.
|
|
228
|
+
|
|
229
|
+
Existe porque os caminhos de find liam o corpo só para logar e lançavam
|
|
230
|
+
``KonectyAPIError`` genérico — e, no caso de HTTP 400, o
|
|
231
|
+
``response.raise_for_status()`` disparava ANTES de o corpo ser lido, então
|
|
232
|
+
nem a mensagem nem o código chegavam a quem chamou.
|
|
233
|
+
"""
|
|
234
|
+
items = errors if isinstance(errors, list) else []
|
|
235
|
+
for error in items:
|
|
236
|
+
if not isinstance(error, dict):
|
|
237
|
+
continue
|
|
238
|
+
exception_class = _ERROR_BY_CODE.get(error.get("code"))
|
|
239
|
+
if exception_class is not None:
|
|
240
|
+
# `message` ausente OU `None`: cai no default da própria exceção, como o SDK
|
|
241
|
+
# TS faz (`new Klass(message ?? undefined)` usa o default do construtor).
|
|
242
|
+
# `str(None)` produziria a string literal "None" na mensagem do chamador.
|
|
243
|
+
message = error.get("message")
|
|
244
|
+
raise exception_class(message) if message else exception_class()
|
|
245
|
+
|
|
246
|
+
raise KonectyAPIError(errors)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class KonectyValidationError(KonectyError):
|
|
250
|
+
"""Raised for validation errors."""
|
|
251
|
+
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class KonectySerializationError(KonectyError):
|
|
256
|
+
"""Raised when a value is not serializable."""
|
|
257
|
+
|
|
258
|
+
def __init__(self) -> None:
|
|
259
|
+
super().__init__("Tipo não serializável")
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Dict, List, Mapping, Optional, TypedDict, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FilterMatch(str, Enum):
|
|
9
|
+
"""Tipo de correspondência do filtro."""
|
|
10
|
+
|
|
11
|
+
AND = "and"
|
|
12
|
+
OR = "or"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FilterOperator(str, Enum):
|
|
16
|
+
"""Operadores disponíveis para filtros."""
|
|
17
|
+
|
|
18
|
+
EQUALS = "equals"
|
|
19
|
+
NOT_EQUALS = "not_equals"
|
|
20
|
+
STARTS_WITH = "starts_with"
|
|
21
|
+
END_WITH = "end_with"
|
|
22
|
+
CONTAINS = "contains"
|
|
23
|
+
NOT_CONTAINS = "not_contains"
|
|
24
|
+
LESS_THAN = "less_than"
|
|
25
|
+
GREATER_THAN = "greater_than"
|
|
26
|
+
LESS_OR_EQUALS = "less_or_equals"
|
|
27
|
+
GREATER_OR_EQUALS = "greater_or_equals"
|
|
28
|
+
BETWEEN = "between"
|
|
29
|
+
IN = "in"
|
|
30
|
+
NOT_IN = "not_in"
|
|
31
|
+
EXISTS = "exists"
|
|
32
|
+
#: Busca por raio geográfico em campo de tipo ``address``. Espelha
|
|
33
|
+
#: ``WITHIN_RADIUS`` no SDK TypeScript — manter os dois em sincronia.
|
|
34
|
+
WITHIN_RADIUS = "within_radius"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
#: Campo calculado que o servidor acrescenta a cada registro devolvido por ``find``
|
|
38
|
+
#: quando o filtro tem **um** centro de distância: a única condição ``within_radius``
|
|
39
|
+
#: habilitada no caminho AND do filtro da requisição. Valor em **metros**, inteiro.
|
|
40
|
+
#: Com zero ou duas+ condições nessa posição, ou quando o usuário não pode ler o campo
|
|
41
|
+
#: ``address`` do centro, o campo simplesmente não vem — não é erro.
|
|
42
|
+
#:
|
|
43
|
+
#: É também o ``property`` de ordenação por distância:
|
|
44
|
+
#: ``SortOrder(property=DISTANCE_FIELD, direction=SortDirection.ASC)``.
|
|
45
|
+
#:
|
|
46
|
+
#: Nunca é gravado: devolver o registro inteiro num ``update`` com ``_distance``
|
|
47
|
+
#: dentro é recusado pelo servidor como campo inexistente.
|
|
48
|
+
#:
|
|
49
|
+
#: Espelha ``DISTANCE_FIELD`` no SDK TypeScript (``src/sdk/filters/withinRadius.ts``).
|
|
50
|
+
DISTANCE_FIELD = "_distance"
|
|
51
|
+
|
|
52
|
+
#: Referência ao registro de onde tirar o centro, em vez de coordenadas literais:
|
|
53
|
+
#: "perto do empreendimento X". O servidor lê o registro sob controle de acesso
|
|
54
|
+
#: completo e substitui pelas coordenadas antes de compilar a query.
|
|
55
|
+
#:
|
|
56
|
+
#: ``field`` é obrigatório porque um documento pode ter mais de um campo
|
|
57
|
+
#: ``address`` e adivinhar seria escolher em silêncio.
|
|
58
|
+
WithinRadiusCenterRef = TypedDict(
|
|
59
|
+
"WithinRadiusCenterRef",
|
|
60
|
+
{"document": str, "_id": str, "field": str},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def within_radius_condition(
|
|
65
|
+
term: str,
|
|
66
|
+
*,
|
|
67
|
+
lat: Optional[float] = None,
|
|
68
|
+
lng: Optional[float] = None,
|
|
69
|
+
record: Optional[WithinRadiusCenterRef] = None,
|
|
70
|
+
radius: float,
|
|
71
|
+
) -> "FilterCondition":
|
|
72
|
+
"""
|
|
73
|
+
Monta a condição de filtro do operador ``within_radius``.
|
|
74
|
+
|
|
75
|
+
O ``term`` é o campo ``address`` puro: o sufixo ``.geolocation`` é
|
|
76
|
+
acrescentado pelo SERVIDOR, não pelo cliente. O ``radius`` é em **metros**.
|
|
77
|
+
O centro é ``lat``/``lng`` **ou** ``record`` — exatamente uma das duas formas.
|
|
78
|
+
|
|
79
|
+
.. code-block:: python
|
|
80
|
+
|
|
81
|
+
within_radius_condition("address", lat=-30.0346, lng=-51.2177, radius=5000)
|
|
82
|
+
# value={"lat": -30.0346, "lng": -51.2177, "radius": 5000}
|
|
83
|
+
|
|
84
|
+
within_radius_condition(
|
|
85
|
+
"address",
|
|
86
|
+
record={"document": "Development", "_id": "<id>", "field": "address"},
|
|
87
|
+
radius=2000,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
Todos os parâmetros depois de ``term`` são nomeados: a forma antiga
|
|
91
|
+
``within_radius_condition(term, (lng, lat), radius)`` deixou de existir e
|
|
92
|
+
levanta ``TypeError``.
|
|
93
|
+
|
|
94
|
+
O SDK **não** valida faixa, teto de raio nem a exclusão mútua das formas:
|
|
95
|
+
quem decide é o servidor, e um teto copiado aqui passaria a mentir assim que
|
|
96
|
+
o backend mudasse. O que o chamador passou vai para a requisição — as duas
|
|
97
|
+
formas juntas inclusive, para o servidor recusar nomeando o problema — e
|
|
98
|
+
nada é convertido para número. Valor recusado volta como
|
|
99
|
+
``WITHIN_RADIUS_INVALID_VALUE``.
|
|
100
|
+
|
|
101
|
+
Espelha ``withinRadiusCondition`` no SDK TypeScript — a mesma entrada produz
|
|
102
|
+
a mesma saída (travado por teste: ``tests/test_within_radius.py`` e
|
|
103
|
+
``src/__test__/api/withinRadius.test.ts``).
|
|
104
|
+
|
|
105
|
+
A ARIDADE difere de propósito: lá é ``withinRadiusCondition(term, {lat, lng,
|
|
106
|
+
radius})``, porque o SDK TypeScript passa objeto de opções em todo lugar;
|
|
107
|
+
aqui são parâmetros nomeados, que é a convenção deste SDK. A paridade que
|
|
108
|
+
importa é mesma entrada → mesma saída, não a forma de chamar.
|
|
109
|
+
"""
|
|
110
|
+
value: Dict[str, Any] = {}
|
|
111
|
+
if lat is not None:
|
|
112
|
+
value["lat"] = lat
|
|
113
|
+
if lng is not None:
|
|
114
|
+
value["lng"] = lng
|
|
115
|
+
if record is not None:
|
|
116
|
+
value["record"] = dict(record) if isinstance(record, Mapping) else record
|
|
117
|
+
value["radius"] = radius
|
|
118
|
+
return FilterCondition(
|
|
119
|
+
term=term,
|
|
120
|
+
operator=FilterOperator.WITHIN_RADIUS,
|
|
121
|
+
value=value,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class DateValue(BaseModel):
|
|
126
|
+
"""Valor de data para filtros."""
|
|
127
|
+
|
|
128
|
+
date: datetime
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class BetweenValue(BaseModel):
|
|
132
|
+
"""Valor para filtros do tipo between."""
|
|
133
|
+
|
|
134
|
+
greater_or_equals: Union[int, float, DateValue, datetime, None]
|
|
135
|
+
less_or_equals: Union[int, float, DateValue, datetime, None]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class FilterCondition(BaseModel):
|
|
139
|
+
"""Condição de filtro."""
|
|
140
|
+
|
|
141
|
+
term: str = Field(..., description="Campo a ser filtrado")
|
|
142
|
+
operator: FilterOperator = Field(..., description="Operador de comparação")
|
|
143
|
+
value: Any = Field(..., description="Valor para comparação")
|
|
144
|
+
disabled: bool = Field(False, description="Se a condição está desativada")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class KonectyFilter(BaseModel):
|
|
148
|
+
"""Filtro Konecty."""
|
|
149
|
+
|
|
150
|
+
match: FilterMatch = Field(FilterMatch.AND, description="Tipo de correspondência")
|
|
151
|
+
conditions: List[FilterCondition] = Field(
|
|
152
|
+
default_factory=list, description="Lista de condições"
|
|
153
|
+
)
|
|
154
|
+
filters: List["KonectyFilter"] = Field(
|
|
155
|
+
default_factory=list, description="Lista de filtros aninhados"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def to_json(self) -> Dict[str, Any]:
|
|
159
|
+
"""Converte o filtro para formato JSON."""
|
|
160
|
+
return self.model_dump(mode="json")
|
|
161
|
+
|
|
162
|
+
def is_empty(self) -> bool:
|
|
163
|
+
"""Verifica se o filtro está vazio."""
|
|
164
|
+
return len(self.conditions) == 0 and len(self.filters) == 0
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_dict(cls, data: Dict[str, Any]) -> "KonectyFilter":
|
|
168
|
+
"""Converte dicionário para filtro."""
|
|
169
|
+
return cls(**data)
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def create(
|
|
173
|
+
cls, match: Union[FilterMatch, str] = FilterMatch.AND
|
|
174
|
+
) -> "KonectyFilter":
|
|
175
|
+
"""Cria uma nova instância de filtro.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
match: Tipo de correspondência ("and" ou "or")
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Nova instância de KonectyFilter
|
|
182
|
+
"""
|
|
183
|
+
if isinstance(match, str):
|
|
184
|
+
match = FilterMatch(match.lower())
|
|
185
|
+
return cls(match=match)
|
|
186
|
+
|
|
187
|
+
def add_condition(
|
|
188
|
+
self,
|
|
189
|
+
term: str,
|
|
190
|
+
operator: Union[FilterOperator, str],
|
|
191
|
+
value: Any,
|
|
192
|
+
disabled: bool = False,
|
|
193
|
+
) -> "KonectyFilter":
|
|
194
|
+
"""Adiciona uma condição ao filtro.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
term: Campo a ser filtrado
|
|
198
|
+
operator: Operador de comparação
|
|
199
|
+
value: Valor para comparação
|
|
200
|
+
disabled: Se a condição está desativada
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Self para encadeamento
|
|
204
|
+
"""
|
|
205
|
+
if isinstance(operator, str):
|
|
206
|
+
operator = FilterOperator(operator.lower())
|
|
207
|
+
|
|
208
|
+
self.conditions.append(
|
|
209
|
+
FilterCondition(
|
|
210
|
+
term=term,
|
|
211
|
+
operator=operator,
|
|
212
|
+
value=value,
|
|
213
|
+
disabled=disabled,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
def add_within_radius(
|
|
219
|
+
self,
|
|
220
|
+
term: str,
|
|
221
|
+
*,
|
|
222
|
+
lat: Optional[float] = None,
|
|
223
|
+
lng: Optional[float] = None,
|
|
224
|
+
record: Optional[WithinRadiusCenterRef] = None,
|
|
225
|
+
radius: float,
|
|
226
|
+
) -> "KonectyFilter":
|
|
227
|
+
"""Adiciona uma condição ``within_radius`` ao filtro.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
term: Campo de tipo ``address`` (sem o sufixo ``.geolocation``)
|
|
231
|
+
lat: Latitude do centro, em ``[-90, 90]``
|
|
232
|
+
lng: Longitude do centro, em ``[-180, 180]``
|
|
233
|
+
record: Ou, no lugar de ``lat``/``lng``, a referência
|
|
234
|
+
``{"document", "_id", "field"}`` a outro registro
|
|
235
|
+
radius: Raio em **metros**
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Self para encadeamento
|
|
239
|
+
|
|
240
|
+
Nota: o SDK TypeScript **não** tem equivalente disto, e é escolha: lá o
|
|
241
|
+
filtro é objeto literal, não há classe de builder nenhuma, e criar uma só
|
|
242
|
+
para este operador seria superfície pública nova sem caso de uso.
|
|
243
|
+
"""
|
|
244
|
+
self.conditions.append(
|
|
245
|
+
within_radius_condition(
|
|
246
|
+
term, lat=lat, lng=lng, record=record, radius=radius
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def add_filter(
|
|
252
|
+
self, match: Union[FilterMatch, str] = FilterMatch.AND
|
|
253
|
+
) -> "KonectyFilter":
|
|
254
|
+
"""Adiciona um filtro aninhado.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
match: Tipo de correspondência do filtro aninhado
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Novo filtro aninhado
|
|
261
|
+
"""
|
|
262
|
+
if isinstance(match, str):
|
|
263
|
+
match = FilterMatch(match.lower())
|
|
264
|
+
|
|
265
|
+
nested_filter = KonectyFilter(match=match)
|
|
266
|
+
self.filters.append(nested_filter)
|
|
267
|
+
return nested_filter
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class SortDirection(str, Enum):
|
|
271
|
+
"""Direção da ordenação."""
|
|
272
|
+
|
|
273
|
+
ASC = "ASC"
|
|
274
|
+
DESC = "DESC"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class SortOrder(BaseModel):
|
|
278
|
+
"""Ordenação de resultados."""
|
|
279
|
+
|
|
280
|
+
property: str = Field(..., description="Campo para ordenação")
|
|
281
|
+
direction: SortDirection = Field(..., description="Direção da ordenação")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class KonectyFindParams(BaseModel):
|
|
285
|
+
"""Parâmetros para busca no Konecty."""
|
|
286
|
+
|
|
287
|
+
filter: KonectyFilter
|
|
288
|
+
start: Optional[int] = None
|
|
289
|
+
limit: Optional[int] = None
|
|
290
|
+
sort: Optional[List[SortOrder]] = None
|
|
291
|
+
fields: Optional[List[Union[str, int]]] = None
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
"""Auth API: Google hosted authorization code flow and
|
|
1
|
+
"""Auth API: Google hosted authorization code flow, login options and session info."""
|
|
2
2
|
|
|
3
3
|
from typing import Any, Dict, Optional, Union
|
|
4
4
|
from urllib.parse import quote, urlencode
|
|
5
5
|
|
|
6
|
+
import aiohttp
|
|
7
|
+
|
|
6
8
|
from ..exceptions import KonectyAPIError, KonectyGoogleSessionError
|
|
7
9
|
from .base import BaseService
|
|
8
10
|
|
|
@@ -13,6 +15,7 @@ GOOGLE_START_PATH = "/api/auth/google/start"
|
|
|
13
15
|
_ENCODE_URI_COMPONENT_SAFE = "!*'()"
|
|
14
16
|
GOOGLE_SESSION_PATH = "/api/auth/google/session"
|
|
15
17
|
LOGIN_OPTIONS_PATH = "/api/auth/login-options"
|
|
18
|
+
USER_INFO_PATH = "/rest/auth/info"
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
_GOOGLE_SESSION_ERROR_CODES = (
|
|
@@ -132,3 +135,27 @@ class AuthService(BaseService):
|
|
|
132
135
|
"""GET /api/auth/login-options. Flags: passwordEnabled, emailOtpEnabled,
|
|
133
136
|
whatsAppOtpEnabled, webauthnEnabled, webauthnRequired, googleEnabled."""
|
|
134
137
|
return await self._get(LOGIN_OPTIONS_PATH)
|
|
138
|
+
|
|
139
|
+
async def info(self) -> Dict[str, Any]:
|
|
140
|
+
"""
|
|
141
|
+
GET /rest/auth/info. The session user and namespace: ``{"logged": True, "user": {...}}``.
|
|
142
|
+
|
|
143
|
+
``user`` carries ``_id``, ``name``, ``email``, ``login``, ``group``, ``role``, ``access``,
|
|
144
|
+
``locale``, ``namespace`` and ``redirectURL`` — the page to open the interface on, or
|
|
145
|
+
None when the user record sets none. Keys are the wire names, as in the TypeScript SDK.
|
|
146
|
+
|
|
147
|
+
Same contract as ``KonectyClient.info()`` in the TypeScript SDK: without a token there is
|
|
148
|
+
no session to ask about, so no request goes out; a 4xx/5xx answer means not logged. Both
|
|
149
|
+
return ``{"logged": False}``.
|
|
150
|
+
"""
|
|
151
|
+
if not self._client.auth_id:
|
|
152
|
+
return {"logged": False}
|
|
153
|
+
try:
|
|
154
|
+
return await self._get(USER_INFO_PATH)
|
|
155
|
+
except KonectyAPIError:
|
|
156
|
+
return {"logged": False}
|
|
157
|
+
except aiohttp.ContentTypeError as error:
|
|
158
|
+
# 401 sem corpo JSON: o http layer tenta ler JSON antes de olhar o status.
|
|
159
|
+
if error.status >= 400:
|
|
160
|
+
return {"logged": False}
|
|
161
|
+
raise
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
"""Exceptions for the Konecty SDK."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class KonectyError(Exception):
|
|
5
|
-
"""Base exception for Konecty errors."""
|
|
6
|
-
|
|
7
|
-
pass
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class KonectyAPIError(KonectyError):
|
|
11
|
-
"""Raised when the API returns success=false or a non-2xx status."""
|
|
12
|
-
|
|
13
|
-
pass
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class KonectyGoogleSessionError(KonectyAPIError):
|
|
17
|
-
"""
|
|
18
|
-
Raised by exchange_google_code. Carries the machine-readable ``code`` from
|
|
19
|
-
``errors[0].code`` so callers can branch or translate without parsing the
|
|
20
|
-
message; ``failed`` covers unreadable or unrecognised bodies.
|
|
21
|
-
|
|
22
|
-
Subclasses KonectyAPIError so existing ``except KonectyAPIError`` keeps working.
|
|
23
|
-
|
|
24
|
-
Mirrors KonectyGoogleSessionError in the TypeScript SDK — keep both in sync.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
def __init__(self, code: str, message: str) -> None:
|
|
28
|
-
super().__init__(message)
|
|
29
|
-
self.code = code
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#: Código devolvido pelo Konecty quando a requisição pede ordenação arbitrária
|
|
33
|
-
#: acima do teto de página. Espelha ``SORT_ABOVE_MAX_PAGE_SIZE`` no SDK
|
|
34
|
-
#: TypeScript — manter os dois em sincronia.
|
|
35
|
-
SORT_ABOVE_MAX_PAGE_SIZE = "SORT_ABOVE_MAX_PAGE_SIZE"
|
|
36
|
-
|
|
37
|
-
#: Teto do servidor. Informativo: quem decide é a API, este valor só evita adivinhação.
|
|
38
|
-
MAX_SORTED_PAGE_SIZE = 1000
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class KonectySortLimitError(KonectyAPIError):
|
|
42
|
-
"""
|
|
43
|
-
Raised when the request asks for arbitrary ordering above the page cap.
|
|
44
|
-
|
|
45
|
-
Acima de ``MAX_SORTED_PAGE_SIZE`` registros — e no caso sem limite
|
|
46
|
-
(``limit=-1``) — o ``$sort`` teria de terminar antes de o primeiro documento
|
|
47
|
-
sair do cursor, então a API recusa com HTTP 400 em vez de trocar a ordenação
|
|
48
|
-
em silêncio, como fazia antes.
|
|
49
|
-
|
|
50
|
-
Carrega o ``code`` legível por máquina para o chamador ramificar sem
|
|
51
|
-
inspecionar a mensagem, como ``KonectyGoogleSessionError``. Subclasse de
|
|
52
|
-
``KonectyAPIError``, então ``except KonectyAPIError`` existente continua
|
|
53
|
-
pegando.
|
|
54
|
-
|
|
55
|
-
A saída é ordenar por ``_id`` (ascendente ou descendente) e paginar por faixa
|
|
56
|
-
de ``_id``, ou pedir no máximo ``MAX_SORTED_PAGE_SIZE`` registros.
|
|
57
|
-
|
|
58
|
-
Espelha ``KonectySortLimitError`` no SDK TypeScript — manter os dois em sincronia.
|
|
59
|
-
"""
|
|
60
|
-
|
|
61
|
-
def __init__(self, message: str) -> None:
|
|
62
|
-
super().__init__(message)
|
|
63
|
-
self.code = SORT_ABOVE_MAX_PAGE_SIZE
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def raise_for_konecty_errors(errors: object) -> None:
|
|
67
|
-
"""
|
|
68
|
-
Lança a exceção mais específica que a lista de ``errors`` da API permitir.
|
|
69
|
-
|
|
70
|
-
Existe porque os caminhos de find liam o corpo só para logar e lançavam
|
|
71
|
-
``KonectyAPIError`` genérico — e, no caso de HTTP 400, o
|
|
72
|
-
``response.raise_for_status()`` disparava ANTES de o corpo ser lido, então
|
|
73
|
-
nem a mensagem nem o código chegavam a quem chamou.
|
|
74
|
-
"""
|
|
75
|
-
items = errors if isinstance(errors, list) else []
|
|
76
|
-
for error in items:
|
|
77
|
-
if isinstance(error, dict) and error.get("code") == SORT_ABOVE_MAX_PAGE_SIZE:
|
|
78
|
-
raise KonectySortLimitError(str(error.get("message", SORT_ABOVE_MAX_PAGE_SIZE)))
|
|
79
|
-
|
|
80
|
-
raise KonectyAPIError(errors)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class KonectyValidationError(KonectyError):
|
|
84
|
-
"""Raised for validation errors."""
|
|
85
|
-
|
|
86
|
-
pass
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class KonectySerializationError(KonectyError):
|
|
90
|
-
"""Raised when a value is not serializable."""
|
|
91
|
-
|
|
92
|
-
def __init__(self) -> None:
|
|
93
|
-
super().__init__("Tipo não serializável")
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
from enum import Enum
|
|
3
|
-
from typing import Any, Dict, List, Optional, Union
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class FilterMatch(str, Enum):
|
|
9
|
-
"""Tipo de correspondência do filtro."""
|
|
10
|
-
|
|
11
|
-
AND = "and"
|
|
12
|
-
OR = "or"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class FilterOperator(str, Enum):
|
|
16
|
-
"""Operadores disponíveis para filtros."""
|
|
17
|
-
|
|
18
|
-
EQUALS = "equals"
|
|
19
|
-
NOT_EQUALS = "not_equals"
|
|
20
|
-
STARTS_WITH = "starts_with"
|
|
21
|
-
END_WITH = "end_with"
|
|
22
|
-
CONTAINS = "contains"
|
|
23
|
-
NOT_CONTAINS = "not_contains"
|
|
24
|
-
LESS_THAN = "less_than"
|
|
25
|
-
GREATER_THAN = "greater_than"
|
|
26
|
-
LESS_OR_EQUALS = "less_or_equals"
|
|
27
|
-
GREATER_OR_EQUALS = "greater_or_equals"
|
|
28
|
-
BETWEEN = "between"
|
|
29
|
-
IN = "in"
|
|
30
|
-
NOT_IN = "not_in"
|
|
31
|
-
EXISTS = "exists"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class DateValue(BaseModel):
|
|
35
|
-
"""Valor de data para filtros."""
|
|
36
|
-
|
|
37
|
-
date: datetime
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class BetweenValue(BaseModel):
|
|
41
|
-
"""Valor para filtros do tipo between."""
|
|
42
|
-
|
|
43
|
-
greater_or_equals: Union[int, float, DateValue, datetime, None]
|
|
44
|
-
less_or_equals: Union[int, float, DateValue, datetime, None]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class FilterCondition(BaseModel):
|
|
48
|
-
"""Condição de filtro."""
|
|
49
|
-
|
|
50
|
-
term: str = Field(..., description="Campo a ser filtrado")
|
|
51
|
-
operator: FilterOperator = Field(..., description="Operador de comparação")
|
|
52
|
-
value: Any = Field(..., description="Valor para comparação")
|
|
53
|
-
disabled: bool = Field(False, description="Se a condição está desativada")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class KonectyFilter(BaseModel):
|
|
57
|
-
"""Filtro Konecty."""
|
|
58
|
-
|
|
59
|
-
match: FilterMatch = Field(FilterMatch.AND, description="Tipo de correspondência")
|
|
60
|
-
conditions: List[FilterCondition] = Field(default_factory=list, description="Lista de condições")
|
|
61
|
-
filters: List["KonectyFilter"] = Field(default_factory=list, description="Lista de filtros aninhados")
|
|
62
|
-
|
|
63
|
-
def to_json(self) -> Dict[str, Any]:
|
|
64
|
-
"""Converte o filtro para formato JSON."""
|
|
65
|
-
return self.model_dump(mode="json")
|
|
66
|
-
|
|
67
|
-
def is_empty(self) -> bool:
|
|
68
|
-
"""Verifica se o filtro está vazio."""
|
|
69
|
-
return len(self.conditions) == 0 and len(self.filters) == 0
|
|
70
|
-
|
|
71
|
-
@classmethod
|
|
72
|
-
def from_dict(cls, data: Dict[str, Any]) -> "KonectyFilter":
|
|
73
|
-
"""Converte dicionário para filtro."""
|
|
74
|
-
return cls(**data)
|
|
75
|
-
|
|
76
|
-
@classmethod
|
|
77
|
-
def create(cls, match: Union[FilterMatch, str] = FilterMatch.AND) -> "KonectyFilter":
|
|
78
|
-
"""Cria uma nova instância de filtro.
|
|
79
|
-
|
|
80
|
-
Args:
|
|
81
|
-
match: Tipo de correspondência ("and" ou "or")
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
Nova instância de KonectyFilter
|
|
85
|
-
"""
|
|
86
|
-
if isinstance(match, str):
|
|
87
|
-
match = FilterMatch(match.lower())
|
|
88
|
-
return cls(match=match)
|
|
89
|
-
|
|
90
|
-
def add_condition(
|
|
91
|
-
self, term: str, operator: Union[FilterOperator, str], value: Any, disabled: bool = False
|
|
92
|
-
) -> "KonectyFilter":
|
|
93
|
-
"""Adiciona uma condição ao filtro.
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
term: Campo a ser filtrado
|
|
97
|
-
operator: Operador de comparação
|
|
98
|
-
value: Valor para comparação
|
|
99
|
-
disabled: Se a condição está desativada
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
Self para encadeamento
|
|
103
|
-
"""
|
|
104
|
-
if isinstance(operator, str):
|
|
105
|
-
operator = FilterOperator(operator.lower())
|
|
106
|
-
|
|
107
|
-
self.conditions.append(
|
|
108
|
-
FilterCondition(
|
|
109
|
-
term=term,
|
|
110
|
-
operator=operator,
|
|
111
|
-
value=value,
|
|
112
|
-
disabled=disabled,
|
|
113
|
-
)
|
|
114
|
-
)
|
|
115
|
-
return self
|
|
116
|
-
|
|
117
|
-
def add_filter(self, match: Union[FilterMatch, str] = FilterMatch.AND) -> "KonectyFilter":
|
|
118
|
-
"""Adiciona um filtro aninhado.
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
match: Tipo de correspondência do filtro aninhado
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
Novo filtro aninhado
|
|
125
|
-
"""
|
|
126
|
-
if isinstance(match, str):
|
|
127
|
-
match = FilterMatch(match.lower())
|
|
128
|
-
|
|
129
|
-
nested_filter = KonectyFilter(match=match)
|
|
130
|
-
self.filters.append(nested_filter)
|
|
131
|
-
return nested_filter
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
class SortDirection(str, Enum):
|
|
135
|
-
"""Direção da ordenação."""
|
|
136
|
-
|
|
137
|
-
ASC = "ASC"
|
|
138
|
-
DESC = "DESC"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
class SortOrder(BaseModel):
|
|
142
|
-
"""Ordenação de resultados."""
|
|
143
|
-
|
|
144
|
-
property: str = Field(..., description="Campo para ordenação")
|
|
145
|
-
direction: SortDirection = Field(..., description="Direção da ordenação")
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
class KonectyFindParams(BaseModel):
|
|
149
|
-
"""Parâmetros para busca no Konecty."""
|
|
150
|
-
|
|
151
|
-
filter: KonectyFilter
|
|
152
|
-
start: Optional[int] = None
|
|
153
|
-
limit: Optional[int] = None
|
|
154
|
-
sort: Optional[List[SortOrder]] = None
|
|
155
|
-
fields: Optional[List[Union[str, int]]] = None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/feature_types/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/feature_types/kpi.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/__init__.py
RENAMED
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/admin.py
RENAMED
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/aggregation.py
RENAMED
|
File without changes
|
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/change_user.py
RENAMED
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/comments.py
RENAMED
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/export.py
RENAMED
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/files.py
RENAMED
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/notifications.py
RENAMED
|
File without changes
|
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/query.py
RENAMED
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/stream.py
RENAMED
|
File without changes
|
{konecty_sdk_python-2.1.5 → konecty_sdk_python-2.1.7}/KonectySdkPython/lib/services/subscriptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|