konecty-sdk-python 2.1.6__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/client.py +51 -11
  2. konecty_sdk_python-2.2.0/KonectySdkPython/lib/exceptions.py +279 -0
  3. konecty_sdk_python-2.2.0/KonectySdkPython/lib/filters.py +291 -0
  4. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/auth.py +96 -4
  5. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/PKG-INFO +1 -1
  6. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/pyproject.toml +1 -1
  7. konecty_sdk_python-2.1.6/KonectySdkPython/lib/exceptions.py +0 -93
  8. konecty_sdk_python-2.1.6/KonectySdkPython/lib/filters.py +0 -155
  9. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/.gitignore +0 -0
  10. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/__init__.py +0 -0
  11. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/cli/__init__.py +0 -0
  12. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/cli/apply.py +0 -0
  13. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/cli/backup.py +0 -0
  14. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/cli/pull.py +0 -0
  15. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/__init__.py +0 -0
  16. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/feature_types/__init__.py +0 -0
  17. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/feature_types/cross_module_query.py +0 -0
  18. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/feature_types/kpi.py +0 -0
  19. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/feature_types/query_json.py +0 -0
  20. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/file_manager.py +0 -0
  21. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/http.py +0 -0
  22. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/model.py +0 -0
  23. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/serialization.py +0 -0
  24. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/__init__.py +0 -0
  25. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/admin.py +0 -0
  26. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/aggregation.py +0 -0
  27. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/base.py +0 -0
  28. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/change_user.py +0 -0
  29. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/comments.py +0 -0
  30. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/export.py +0 -0
  31. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/files.py +0 -0
  32. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/notifications.py +0 -0
  33. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/pat.py +0 -0
  34. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/query.py +0 -0
  35. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/stream.py +0 -0
  36. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/services/subscriptions.py +0 -0
  37. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/settings.py +0 -0
  38. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/KonectySdkPython/lib/types.py +0 -0
  39. {konecty_sdk_python-2.1.6 → konecty_sdk_python-2.2.0}/README.md +0 -0
@@ -86,11 +86,20 @@ async def _read_json_body(response: "aiohttp.ClientResponse") -> Optional[Dict[s
86
86
  Lê o corpo como JSON sem explodir quando não é JSON (proxy, gateway, HTML).
87
87
 
88
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.
89
95
  """
90
96
  try:
91
- return cast(Dict[str, Any], await response.json(content_type=None))
97
+ body = await response.json(content_type=None)
92
98
  except Exception: # noqa: BLE001 - corpo não-JSON é caso esperado aqui
93
99
  return None
100
+ if not isinstance(body, dict):
101
+ return None
102
+ return cast(Dict[str, Any], body)
94
103
 
95
104
 
96
105
  class KonectyClient:
@@ -454,10 +463,30 @@ class KonectyClient:
454
463
  fingerprint=fingerprint,
455
464
  )
456
465
 
466
+ async def impersonate(
467
+ self, user_id: Optional[str] = None, code: Optional[int] = None
468
+ ) -> Dict[str, Any]:
469
+ """
470
+ Issue a support session (1 h) for another user (POST /api/auth/impersonate).
471
+
472
+ Exactly one of ``user_id`` or ``code``. Requires an admin first-party session.
473
+ Returns ``{"authId", "expiresAt", "user"}``; the returned authId is not adopted by
474
+ this client. Raises KonectyImpersonateError with a machine-readable ``code``.
475
+ """
476
+ return await self._auth.impersonate(user_id=user_id, code=code)
477
+
457
478
  async def get_login_options(self) -> Dict[str, Any]:
458
479
  """Get login method flags for the namespace (GET /api/auth/login-options)."""
459
480
  return await self._auth.get_login_options()
460
481
 
482
+ async def info(self) -> Dict[str, Any]:
483
+ """
484
+ Get the session user (GET /rest/auth/info), including ``user["redirectURL"]``.
485
+
486
+ Returns ``{"logged": False}`` without a token or when the server refuses the session.
487
+ """
488
+ return await self._auth.info()
489
+
461
490
  @property
462
491
  def _pat(self) -> PatService:
463
492
  if not hasattr(self, "_pat_service"):
@@ -719,7 +748,6 @@ class KonectyClient:
719
748
  if search:
720
749
  params["search"] = search
721
750
 
722
- print(f"params: {params}")
723
751
  async with (
724
752
  aiohttp.ClientSession() as session,
725
753
  session.get(
@@ -728,12 +756,16 @@ class KonectyClient:
728
756
  headers={"Authorization": self.headers["Authorization"]},
729
757
  ) as response,
730
758
  ):
731
- response.raise_for_status()
732
- result = await response.json()
759
+ # Mesma ordem do `find`: corpo primeiro, status depois. Ver `find`.
760
+ result = await _read_json_body(response)
761
+ if result is None:
762
+ response.raise_for_status()
763
+ raise KonectyAPIError(f"{response.status} {response.reason}")
733
764
  if not result.get("success", False):
734
765
  errors = result.get("errors", [])
735
766
  logger.error(errors)
736
- raise KonectyAPIError(errors)
767
+ raise_for_konecty_errors(errors)
768
+ response.raise_for_status()
737
769
  data = result.get("data", [])
738
770
  return cast(List[KonectyDict], data)
739
771
 
@@ -784,12 +816,16 @@ class KonectyClient:
784
816
  headers={"Authorization": self.headers["Authorization"]},
785
817
  ) as response,
786
818
  ):
787
- response.raise_for_status()
788
- result = await response.json()
819
+ # Mesma ordem do `find`: corpo primeiro, status depois. Ver `find`.
820
+ result = await _read_json_body(response)
821
+ if result is None:
822
+ response.raise_for_status()
823
+ raise KonectyAPIError(f"{response.status} {response.reason}")
789
824
  if not result.get("success", False):
790
825
  errors = result.get("errors", [])
791
826
  logger.error(errors)
792
- raise KonectyAPIError(errors)
827
+ raise_for_konecty_errors(errors)
828
+ response.raise_for_status()
793
829
  data = result.get("data", [None])
794
830
  return get_first_dict(data)
795
831
 
@@ -1022,12 +1058,16 @@ class KonectyClient:
1022
1058
  headers={"Authorization": self.headers["Authorization"]},
1023
1059
  ) as response,
1024
1060
  ):
1025
- response.raise_for_status()
1026
- result = await response.json()
1061
+ # Mesma ordem do `find`: corpo primeiro, status depois. Ver `find`.
1062
+ result = await _read_json_body(response)
1063
+ if result is None:
1064
+ response.raise_for_status()
1065
+ raise KonectyAPIError(f"{response.status} {response.reason}")
1027
1066
  if not result.get("success", False):
1028
1067
  errors = result.get("errors", [])
1029
1068
  logger.error(errors)
1030
- raise KonectyAPIError(errors)
1069
+ raise_for_konecty_errors(errors)
1070
+ response.raise_for_status()
1031
1071
  count = result.get("total", 0)
1032
1072
  return count
1033
1073
 
@@ -0,0 +1,279 @@
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
+ class KonectyImpersonateError(KonectyAPIError):
35
+ """
36
+ Raised by impersonate. Carries the machine-readable ``code`` from ``errors[0].code``
37
+ (``IMPERSONATE_FORBIDDEN``, ``IMPERSONATE_INVALID_TARGET``,
38
+ ``IMPERSONATE_TARGET_NOT_FOUND``, ``IMPERSONATE_TARGET_NOT_ALLOWED``,
39
+ ``IMPERSONATED_SESSION_FORBIDDEN``, ``admin-credential-routes-require-session``);
40
+ ``failed`` covers transport errors, unreadable bodies and answers without a recognised
41
+ code, e.g. a plain 401. ``IMPERSONATE_INVALID_TARGET`` is also raised locally, with no
42
+ request, when the target does not have exactly one of ``user_id`` or ``code``.
43
+
44
+ Subclasses KonectyAPIError so existing ``except KonectyAPIError`` keeps working.
45
+
46
+ Mirrors KonectyImpersonateError in the TypeScript SDK — keep both in sync.
47
+ """
48
+
49
+ def __init__(self, code: str, message: str) -> None:
50
+ super().__init__(message)
51
+ self.code = code
52
+
53
+
54
+ #: Código devolvido pelo Konecty quando a requisição pede ordenação arbitrária
55
+ #: acima do teto de página. Espelha ``SORT_ABOVE_MAX_PAGE_SIZE`` no SDK
56
+ #: TypeScript — manter os dois em sincronia.
57
+ SORT_ABOVE_MAX_PAGE_SIZE = "SORT_ABOVE_MAX_PAGE_SIZE"
58
+
59
+ #: Teto do servidor. Informativo: quem decide é a API, este valor só evita adivinhação.
60
+ MAX_SORTED_PAGE_SIZE = 1000
61
+
62
+
63
+ class KonectySortLimitError(KonectyAPIError):
64
+ """
65
+ Raised when the request asks for arbitrary ordering above the page cap.
66
+
67
+ Acima de ``MAX_SORTED_PAGE_SIZE`` registros — e no caso sem limite
68
+ (``limit=-1``) — o ``$sort`` teria de terminar antes de o primeiro documento
69
+ sair do cursor, então a API recusa com HTTP 400 em vez de trocar a ordenação
70
+ em silêncio, como fazia antes.
71
+
72
+ Carrega o ``code`` legível por máquina para o chamador ramificar sem
73
+ inspecionar a mensagem, como ``KonectyGoogleSessionError``. Subclasse de
74
+ ``KonectyAPIError``, então ``except KonectyAPIError`` existente continua
75
+ pegando.
76
+
77
+ A saída é ordenar por ``_id`` (ascendente ou descendente) e paginar por faixa
78
+ de ``_id``, ou pedir no máximo ``MAX_SORTED_PAGE_SIZE`` registros.
79
+
80
+ Espelha ``KonectySortLimitError`` no SDK TypeScript — manter os dois em sincronia.
81
+ """
82
+
83
+ def __init__(self, message: str) -> None:
84
+ super().__init__(message)
85
+ self.code = SORT_ABOVE_MAX_PAGE_SIZE
86
+
87
+
88
+ #: Códigos do operador de filtro ``within_radius``. Espelham as constantes de
89
+ #: mesmo nome no SDK TypeScript (``src/sdk/filters/withinRadius.ts``) e no
90
+ #: servidor (``src/imports/data/filters/withinRadius.ts`` no repo Konecty).
91
+ #:
92
+ #: Recusa de forma ou de faixa do valor: ``lat``/``lng`` (ou ``record``) ou
93
+ #: ``radius`` ausente, as duas formas juntas, chave desconhecida, coordenada fora
94
+ #: da faixa, string numérica no lugar de número, raio não positivo ou acima do
95
+ #: teto do servidor.
96
+ #: Nome do operador, usado nas mensagens default. Espelha `WITHIN_RADIUS` no SDK TS.
97
+ WITHIN_RADIUS_OPERATOR = "within_radius"
98
+
99
+ WITHIN_RADIUS_INVALID_VALUE = "WITHIN_RADIUS_INVALID_VALUE"
100
+
101
+ #: O centro por referência a registro não pôde ser resolvido. Um código só para
102
+ #: os três casos (registro inexistente, ilegível, ou sem geolocalização) é
103
+ #: decisão do servidor: distinguí-los na resposta transformaria o filtro num
104
+ #: oráculo de localização para quem não pode ler o registro.
105
+ WITHIN_RADIUS_CENTER_UNRESOLVED = "WITHIN_RADIUS_CENTER_UNRESOLVED"
106
+
107
+ #: A cadeia de centros por referência passou do teto de profundidade do servidor.
108
+ #: Acontece quando um centro aponta para um registro cujo filtro de leitura tem, ele
109
+ #: próprio, um `within_radius` com centro por referência — a resolução é recursiva e o
110
+ #: servidor corta. Não é erro do valor enviado: é configuração de metadado do documento
111
+ #: alvo. Espelha `WITHIN_RADIUS_CENTER_DEPTH_EXCEEDED` no SDK TS.
112
+ WITHIN_RADIUS_CENTER_DEPTH_EXCEEDED = "WITHIN_RADIUS_CENTER_DEPTH_EXCEEDED"
113
+
114
+ #: A requisição pediu mais centros por referência do que o servidor resolve de uma vez.
115
+ #: O SDK **não** duplica o teto: quem decide é o servidor, e um número copiado aqui
116
+ #: passaria a mentir assim que o backend mudasse. Espelha `WITHIN_RADIUS_TOO_MANY_CENTERS`
117
+ #: no SDK TS.
118
+ WITHIN_RADIUS_TOO_MANY_CENTERS = "WITHIN_RADIUS_TOO_MANY_CENTERS"
119
+
120
+ #: Recusa de ordenação por ``_distance``: o filtro não tem exatamente um
121
+ #: ``within_radius`` no caminho AND, ou o usuário não pode ler (ou só lê sob
122
+ #: condição) o campo ``address`` do centro. O servidor responde HTTP 400; a mensagem
123
+ #: diz qual dos dois casos é. Espelha ``DISTANCE_SORT_UNAVAILABLE`` no SDK TS.
124
+ DISTANCE_SORT_UNAVAILABLE = "DISTANCE_SORT_UNAVAILABLE"
125
+
126
+
127
+ class KonectyWithinRadiusValueError(KonectyAPIError):
128
+ """
129
+ Raised when the server refuses the shape or range of a ``within_radius`` value.
130
+
131
+ Carrega o ``code`` legível por máquina e preserva a mensagem do servidor, que
132
+ nomeia o termo e a chave exata que falhou. Subclasse de ``KonectyAPIError``,
133
+ então ``except KonectyAPIError`` existente continua pegando.
134
+
135
+ O SDK **não** duplica a validação de faixa nem o teto de raio: quem decide é
136
+ o servidor, e um teto copiado no cliente passa a mentir assim que o backend
137
+ muda. Mesma postura do ``SORT_ABOVE_MAX_PAGE_SIZE``.
138
+
139
+ Espelha ``KonectyWithinRadiusValueError`` no SDK TypeScript — manter os dois
140
+ em sincronia.
141
+ """
142
+
143
+ #: Default idêntico ao do SDK TS (`message ?? \`Invalid value for operator ${WITHIN_RADIUS}\``
144
+ #: em `src/sdk/filters/withinRadius.ts`), para que um erro sem `message` produza a MESMA
145
+ #: frase nos dois — antes o Python levantava com a string literal "None".
146
+ def __init__(self, message: Optional[str] = None) -> None:
147
+ super().__init__(
148
+ message or f"Invalid value for operator {WITHIN_RADIUS_OPERATOR}"
149
+ )
150
+ self.code = WITHIN_RADIUS_INVALID_VALUE
151
+
152
+
153
+ class KonectyWithinRadiusCenterError(KonectyAPIError):
154
+ """
155
+ Raised when a ``within_radius`` center given as a record reference could not
156
+ be resolved — the record does not exist, is not readable, or has no geolocation.
157
+
158
+ Espelha ``KonectyWithinRadiusCenterError`` no SDK TypeScript — manter os dois
159
+ em sincronia.
160
+ """
161
+
162
+ #: Mesmo default do SDK TS — ver `KonectyWithinRadiusValueError` acima.
163
+ def __init__(self, message: Optional[str] = None) -> None:
164
+ super().__init__(
165
+ message
166
+ or f"Could not resolve the center record for operator {WITHIN_RADIUS_OPERATOR}"
167
+ )
168
+ self.code = WITHIN_RADIUS_CENTER_UNRESOLVED
169
+
170
+
171
+ class KonectyWithinRadiusCenterDepthError(KonectyWithinRadiusCenterError):
172
+ """
173
+ Raised when the chain of center-by-reference resolutions exceeds the server depth cap.
174
+
175
+ **Herda de ``KonectyWithinRadiusCenterError`` de propósito**, como no SDK TS: é uma
176
+ falha de resolução de centro, e a própria mensagem do servidor começa com "Could not
177
+ resolve the center record". Quem escreveu ``except KonectyWithinRadiusCenterError``
178
+ antes deste código existir continua pegando.
179
+
180
+ Espelha ``KonectyWithinRadiusCenterDepthError`` no SDK TypeScript.
181
+ """
182
+
183
+ def __init__(self, message: Optional[str] = None) -> None:
184
+ super().__init__(
185
+ message
186
+ or f"Center hydration exceeded the maximum depth for operator {WITHIN_RADIUS_OPERATOR}"
187
+ )
188
+ self.code = WITHIN_RADIUS_CENTER_DEPTH_EXCEEDED
189
+
190
+
191
+ class KonectyWithinRadiusTooManyCentersError(KonectyAPIError):
192
+ """
193
+ Raised when the request asks for more center records than the server resolves at once.
194
+
195
+ **NÃO** herda de ``KonectyWithinRadiusCenterError``: nenhum centro específico falhou —
196
+ a requisição inteira foi recusada antes de qualquer leitura. Tratar cota como falha de
197
+ centro levaria o chamador a procurar um registro culpado que não existe.
198
+
199
+ Espelha ``KonectyWithinRadiusTooManyCentersError`` no SDK TypeScript.
200
+ """
201
+
202
+ def __init__(self, message: Optional[str] = None) -> None:
203
+ super().__init__(
204
+ message
205
+ or f"Too many center records requested for operator {WITHIN_RADIUS_OPERATOR}"
206
+ )
207
+ self.code = WITHIN_RADIUS_TOO_MANY_CENTERS
208
+
209
+
210
+ class KonectyDistanceSortUnavailableError(KonectyAPIError):
211
+ """
212
+ Raised when the server refuses a sort by ``_distance``.
213
+
214
+ **NÃO** herda de ``KonectySortLimitError``: não é teto de página, e a saída não é
215
+ ordenar por ``_id`` — é acertar o filtro (um ``within_radius`` só, no caminho AND)
216
+ ou a permissão de leitura do campo ``address``.
217
+
218
+ Espelha ``KonectyDistanceSortUnavailableError`` no SDK TypeScript.
219
+ """
220
+
221
+ #: Mesmo default do SDK TS (`src/sdk/filters/withinRadius.ts`).
222
+ def __init__(self, message: Optional[str] = None) -> None:
223
+ super().__init__(
224
+ message or "Sorting by _distance is not available for this query"
225
+ )
226
+ self.code = DISTANCE_SORT_UNAVAILABLE
227
+
228
+
229
+ #: Códigos que o SDK promove a exceção própria. A tabela existe para que um
230
+ #: código novo seja UMA linha em vez de mais um ``if`` no meio do fluxo — e para
231
+ #: que a lista de códigos suportados seja legível de uma vez, ao lado da lista
232
+ #: equivalente no SDK TypeScript (``ERROR_BY_CODE`` em ``src/sdk/errors.ts``).
233
+ #:
234
+ #: Todo código ausente daqui continua virando ``KonectyAPIError`` genérico.
235
+ _ERROR_BY_CODE = {
236
+ SORT_ABOVE_MAX_PAGE_SIZE: KonectySortLimitError,
237
+ WITHIN_RADIUS_INVALID_VALUE: KonectyWithinRadiusValueError,
238
+ WITHIN_RADIUS_CENTER_UNRESOLVED: KonectyWithinRadiusCenterError,
239
+ WITHIN_RADIUS_CENTER_DEPTH_EXCEEDED: KonectyWithinRadiusCenterDepthError,
240
+ WITHIN_RADIUS_TOO_MANY_CENTERS: KonectyWithinRadiusTooManyCentersError,
241
+ DISTANCE_SORT_UNAVAILABLE: KonectyDistanceSortUnavailableError,
242
+ }
243
+
244
+
245
+ def raise_for_konecty_errors(errors: object) -> None:
246
+ """
247
+ Lança a exceção mais específica que a lista de ``errors`` da API permitir.
248
+
249
+ Existe porque os caminhos de find liam o corpo só para logar e lançavam
250
+ ``KonectyAPIError`` genérico — e, no caso de HTTP 400, o
251
+ ``response.raise_for_status()`` disparava ANTES de o corpo ser lido, então
252
+ nem a mensagem nem o código chegavam a quem chamou.
253
+ """
254
+ items = errors if isinstance(errors, list) else []
255
+ for error in items:
256
+ if not isinstance(error, dict):
257
+ continue
258
+ exception_class = _ERROR_BY_CODE.get(error.get("code"))
259
+ if exception_class is not None:
260
+ # `message` ausente OU `None`: cai no default da própria exceção, como o SDK
261
+ # TS faz (`new Klass(message ?? undefined)` usa o default do construtor).
262
+ # `str(None)` produziria a string literal "None" na mensagem do chamador.
263
+ message = error.get("message")
264
+ raise exception_class(message) if message else exception_class()
265
+
266
+ raise KonectyAPIError(errors)
267
+
268
+
269
+ class KonectyValidationError(KonectyError):
270
+ """Raised for validation errors."""
271
+
272
+ pass
273
+
274
+
275
+ class KonectySerializationError(KonectyError):
276
+ """Raised when a value is not serializable."""
277
+
278
+ def __init__(self) -> None:
279
+ 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,9 +1,15 @@
1
- """Auth API: Google hosted authorization code flow and login options."""
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
- from ..exceptions import KonectyAPIError, KonectyGoogleSessionError
6
+ import aiohttp
7
+
8
+ from ..exceptions import (
9
+ KonectyAPIError,
10
+ KonectyGoogleSessionError,
11
+ KonectyImpersonateError,
12
+ )
7
13
  from .base import BaseService
8
14
 
9
15
  GOOGLE_START_PATH = "/api/auth/google/start"
@@ -13,6 +19,8 @@ GOOGLE_START_PATH = "/api/auth/google/start"
13
19
  _ENCODE_URI_COMPONENT_SAFE = "!*'()"
14
20
  GOOGLE_SESSION_PATH = "/api/auth/google/session"
15
21
  LOGIN_OPTIONS_PATH = "/api/auth/login-options"
22
+ USER_INFO_PATH = "/rest/auth/info"
23
+ IMPERSONATE_PATH = "/api/auth/impersonate"
16
24
 
17
25
 
18
26
  _GOOGLE_SESSION_ERROR_CODES = (
@@ -23,7 +31,19 @@ _GOOGLE_SESSION_ERROR_CODES = (
23
31
  )
24
32
 
25
33
 
26
- def _first_error_code(error: KonectyAPIError) -> str:
34
+ _IMPERSONATE_ERROR_CODES = (
35
+ "IMPERSONATE_FORBIDDEN",
36
+ "IMPERSONATE_INVALID_TARGET",
37
+ "IMPERSONATE_TARGET_NOT_FOUND",
38
+ "IMPERSONATE_TARGET_NOT_ALLOWED",
39
+ "IMPERSONATED_SESSION_FORBIDDEN",
40
+ "admin-credential-routes-require-session",
41
+ )
42
+
43
+
44
+ def _first_error_code(
45
+ error: KonectyAPIError, recognised: tuple = _GOOGLE_SESSION_ERROR_CODES
46
+ ) -> str:
27
47
  """
28
48
  Extract the first recognised ``errors[].code``; ``failed`` when there is none.
29
49
 
@@ -33,7 +53,7 @@ def _first_error_code(error: KonectyAPIError) -> str:
33
53
  errors = error.args[0] if error.args else None
34
54
  if isinstance(errors, list):
35
55
  for item in errors:
36
- if isinstance(item, dict) and item.get("code") in _GOOGLE_SESSION_ERROR_CODES:
56
+ if isinstance(item, dict) and item.get("code") in recognised:
37
57
  return str(item["code"])
38
58
  return "failed"
39
59
 
@@ -128,7 +148,79 @@ class AuthService(BaseService):
128
148
  self._client.set_auth_id(auth_id)
129
149
  return response
130
150
 
151
+ async def impersonate(
152
+ self, user_id: Optional[str] = None, code: Optional[int] = None
153
+ ) -> Dict[str, Any]:
154
+ """
155
+ POST /api/auth/impersonate. Issues a support session (1 h) for another user.
156
+
157
+ Pass exactly one of ``user_id`` (the user's ``_id``, sent as ``userId``) or ``code``
158
+ (the user's numeric code). The caller must be an admin authenticated with a
159
+ first-party session. Returns ``{"authId", "expiresAt", "user"}`` (``expiresAt`` is
160
+ ISO 8601). The returned ``authId`` is NOT adopted by this client — the admin session
161
+ stays intact.
162
+
163
+ Raises KonectyImpersonateError — errors[0].message as the message plus the
164
+ machine-readable ``code`` — on any failure. Zero or two targets raise
165
+ ``IMPERSONATE_INVALID_TARGET`` without sending a request. The error subclasses
166
+ KonectyAPIError, so callers catching that keep working.
167
+ """
168
+ if (user_id is not None) == (code is not None):
169
+ raise KonectyImpersonateError(
170
+ "IMPERSONATE_INVALID_TARGET", "Pass exactly one of userId or code"
171
+ )
172
+ payload: Dict[str, Any] = (
173
+ {"userId": user_id} if user_id is not None else {"code": code}
174
+ )
175
+
176
+ try:
177
+ response = await self._post(IMPERSONATE_PATH, json=payload)
178
+ except KonectyAPIError as error:
179
+ raise KonectyImpersonateError(
180
+ _first_error_code(error, _IMPERSONATE_ERROR_CODES),
181
+ _first_error_message(error),
182
+ ) from error
183
+
184
+ data = response.get("data") if isinstance(response, dict) else None
185
+ auth_id = data.get("authId") if isinstance(data, dict) else None
186
+ if not isinstance(auth_id, str) or not auth_id:
187
+ raise KonectyImpersonateError(
188
+ "failed", "Impersonate response has no authId"
189
+ )
190
+
191
+ return {
192
+ "authId": auth_id,
193
+ "expiresAt": data.get("expiresAt"),
194
+ "user": data.get("user"),
195
+ }
196
+
131
197
  async def get_login_options(self) -> Dict[str, Any]:
132
198
  """GET /api/auth/login-options. Flags: passwordEnabled, emailOtpEnabled,
133
199
  whatsAppOtpEnabled, webauthnEnabled, webauthnRequired, googleEnabled."""
134
200
  return await self._get(LOGIN_OPTIONS_PATH)
201
+
202
+ async def info(self) -> Dict[str, Any]:
203
+ """
204
+ GET /rest/auth/info. The session user and namespace: ``{"logged": True, "user": {...}}``.
205
+
206
+ ``user`` carries ``_id``, ``name``, ``email``, ``login``, ``group``, ``role``, ``access``,
207
+ ``locale``, ``namespace`` and ``redirectURL`` — the page to open the interface on, or
208
+ None when the user record sets none. In a support session (see ``impersonate``) it also
209
+ carries ``impersonatedBy`` (``{"_id", "name"}`` of the admin) and ``impersonationExpiresAt``
210
+ (ISO 8601). Keys are the wire names, as in the TypeScript SDK.
211
+
212
+ Same contract as ``KonectyClient.info()`` in the TypeScript SDK: without a token there is
213
+ no session to ask about, so no request goes out; a 4xx/5xx answer means not logged. Both
214
+ return ``{"logged": False}``.
215
+ """
216
+ if not self._client.auth_id:
217
+ return {"logged": False}
218
+ try:
219
+ return await self._get(USER_INFO_PATH)
220
+ except KonectyAPIError:
221
+ return {"logged": False}
222
+ except aiohttp.ContentTypeError as error:
223
+ # 401 sem corpo JSON: o http layer tenta ler JSON antes de olhar o status.
224
+ if error.status >= 400:
225
+ return {"logged": False}
226
+ raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: konecty_sdk_python
3
- Version: 2.1.6
3
+ Version: 2.2.0
4
4
  Summary: Konecty SDK Python
5
5
  Author-email: Leonardo Leal <leonardo.leal@konecty.com>, Derotino Silveira <derotino.silveira@konecty.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "konecty_sdk_python"
3
- version = "2.1.6"
3
+ version = "2.2.0"
4
4
  description = "Konecty SDK Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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