konecty_sdk_python 2.1.1__py3-none-any.whl

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 (35) hide show
  1. KonectySdkPython/__init__.py +41 -0
  2. KonectySdkPython/cli/__init__.py +38 -0
  3. KonectySdkPython/cli/apply.py +555 -0
  4. KonectySdkPython/cli/backup.py +161 -0
  5. KonectySdkPython/cli/pull.py +309 -0
  6. KonectySdkPython/lib/__init__.py +43 -0
  7. KonectySdkPython/lib/client.py +950 -0
  8. KonectySdkPython/lib/exceptions.py +26 -0
  9. KonectySdkPython/lib/feature_types/__init__.py +46 -0
  10. KonectySdkPython/lib/feature_types/cross_module_query.py +78 -0
  11. KonectySdkPython/lib/feature_types/kpi.py +21 -0
  12. KonectySdkPython/lib/feature_types/query_json.py +242 -0
  13. KonectySdkPython/lib/file_manager.py +248 -0
  14. KonectySdkPython/lib/filters.py +155 -0
  15. KonectySdkPython/lib/http.py +95 -0
  16. KonectySdkPython/lib/model.py +88 -0
  17. KonectySdkPython/lib/serialization.py +13 -0
  18. KonectySdkPython/lib/services/__init__.py +5 -0
  19. KonectySdkPython/lib/services/aggregation.py +142 -0
  20. KonectySdkPython/lib/services/auth.py +105 -0
  21. KonectySdkPython/lib/services/base.py +63 -0
  22. KonectySdkPython/lib/services/change_user.py +64 -0
  23. KonectySdkPython/lib/services/comments.py +89 -0
  24. KonectySdkPython/lib/services/export.py +54 -0
  25. KonectySdkPython/lib/services/files.py +59 -0
  26. KonectySdkPython/lib/services/notifications.py +41 -0
  27. KonectySdkPython/lib/services/query.py +192 -0
  28. KonectySdkPython/lib/services/stream.py +133 -0
  29. KonectySdkPython/lib/services/subscriptions.py +26 -0
  30. KonectySdkPython/lib/settings.py +152 -0
  31. KonectySdkPython/lib/types.py +418 -0
  32. konecty_sdk_python-2.1.1.dist-info/METADATA +155 -0
  33. konecty_sdk_python-2.1.1.dist-info/RECORD +35 -0
  34. konecty_sdk_python-2.1.1.dist-info/WHEEL +4 -0
  35. konecty_sdk_python-2.1.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,152 @@
1
+ """Módulo para gerenciar configurações do Konecty."""
2
+
3
+ import json
4
+ import os
5
+ from typing import Any, Type, TypeVar
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from .client import KonectyClient
10
+
11
+ T = TypeVar("T", bound=BaseModel)
12
+
13
+
14
+ def _convert_value(value: str, field_type: Type[Any] | None) -> Any:
15
+ """Converte um valor string para o tipo correto.
16
+
17
+ Args:
18
+ value: Valor string a ser convertido
19
+ field_type: Tipo para converter o valor
20
+
21
+ Returns:
22
+ Valor convertido para o tipo correto
23
+ """
24
+ if field_type is None:
25
+ return value
26
+
27
+ try:
28
+ if field_type is bool:
29
+ return value.lower() == "true"
30
+ elif field_type is int:
31
+ return int(value)
32
+ elif field_type is float:
33
+ return float(value)
34
+ elif field_type is list:
35
+ return [item.strip() for item in value.split(",")]
36
+ elif field_type is dict:
37
+ return json.loads(value)
38
+ else:
39
+ return value
40
+ except (ValueError, TypeError, json.JSONDecodeError):
41
+ return None
42
+
43
+
44
+ async def fill_settings(settings_class: Type[T]) -> T:
45
+ """Preenche as configurações de uma classe com valores do Konecty.
46
+
47
+ Args:
48
+ settings_class: Classe de configurações que herda de BaseModel.
49
+
50
+ Returns:
51
+ Instância da classe de configurações preenchida com valores do Konecty.
52
+ """
53
+ client = KonectyClient(
54
+ base_url=os.getenv("KONECTY_URL", "http://localhost:3000"),
55
+ token=os.getenv("KONECTY_TOKEN", "invalid_key"),
56
+ )
57
+
58
+ settings_dict = {}
59
+ fields_to_fetch = []
60
+
61
+ # Primeiro verifica variáveis de ambiente e coleta campos que precisam ser buscados no Konecty
62
+ for field_name in settings_class.model_fields.keys():
63
+ field = settings_class.model_fields[field_name]
64
+ field_str = get_field_name(field, field_name)
65
+
66
+ env_value = os.getenv(field_str.upper())
67
+ if env_value is not None and env_value.strip():
68
+ field_type = field.annotation
69
+ converted_value = _convert_value(env_value, field_type)
70
+ if converted_value is not None:
71
+ settings_dict[field_name] = converted_value
72
+ else:
73
+ fields_to_fetch.append(field_name)
74
+
75
+ # Busca todas as configurações do Konecty de uma vez
76
+ if fields_to_fetch:
77
+ konecty_settings = await client.get_settings(
78
+ [field.upper() for field in fields_to_fetch]
79
+ )
80
+
81
+ for field_name in settings_class.model_fields.keys():
82
+ field = settings_class.model_fields[field_name]
83
+ field_str = get_field_name(field, field_name)
84
+
85
+ value = konecty_settings.get(field_str.upper())
86
+ if value is not None and value.strip():
87
+ field_type = field.annotation
88
+ converted_value = _convert_value(value, field_type)
89
+ if converted_value is not None:
90
+ settings_dict[field_name] = converted_value
91
+
92
+ return settings_class.model_construct(**settings_dict)
93
+
94
+
95
+ def fill_settings_sync(settings_class: Type[T]) -> T:
96
+ """Versão síncrona de fill_settings.
97
+
98
+ Args:
99
+ settings_class: Classe de configurações que herda de BaseModel.
100
+
101
+ Returns:
102
+ Instância da classe de configurações preenchida com valores do Konecty.
103
+ """
104
+ client = KonectyClient(
105
+ base_url=os.getenv("KONECTY_URL", "http://localhost:3000"),
106
+ token=os.getenv("KONECTY_TOKEN", "invalid_key"),
107
+ )
108
+
109
+ settings_dict = {}
110
+ fields_to_fetch = []
111
+
112
+ # Primeiro verifica variáveis de ambiente e coleta campos que precisam ser buscados no Konecty
113
+ for field_name in settings_class.model_fields.keys():
114
+ field = settings_class.model_fields[field_name]
115
+ field_str = get_field_name(field, field_name)
116
+
117
+ env_value = os.getenv(field_str.upper())
118
+ if env_value is not None and env_value.strip():
119
+ field_type = field.annotation
120
+ converted_value = _convert_value(env_value, field_type)
121
+ if converted_value is not None:
122
+ settings_dict[field_name] = converted_value
123
+ else:
124
+ # Se não encontrou na variável de ambiente, adiciona à lista para buscar no Konecty
125
+ fields_to_fetch.append(field_name)
126
+
127
+ # Busca todas as configurações do Konecty de uma vez
128
+ if fields_to_fetch:
129
+ konecty_settings = client.get_settings_sync(
130
+ [field.upper() for field in fields_to_fetch]
131
+ )
132
+
133
+ for field_name in settings_class.model_fields.keys():
134
+ field = settings_class.model_fields[field_name]
135
+ field_str = get_field_name(field, field_name)
136
+
137
+ value = konecty_settings.get(field_str.upper())
138
+ if value is not None and value.strip():
139
+ field_type = field.annotation
140
+ converted_value = _convert_value(value, field_type)
141
+ if converted_value is not None:
142
+ settings_dict[field_name] = converted_value
143
+
144
+ return settings_class.model_construct(**settings_dict)
145
+
146
+
147
+ def get_field_name(field, default: str = None) -> str:
148
+ return (
149
+ field.validation_alias
150
+ if field.validation_alias
151
+ else field.alias if field.alias else default
152
+ )
@@ -0,0 +1,418 @@
1
+ import json
2
+ import re
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from typing import Any, Dict, List, Optional, Self
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+ from pydantic.json_schema import JsonSchemaValue
9
+ from pydantic_core import CoreSchema, core_schema
10
+
11
+ KonectyDict = Dict[str, Any]
12
+
13
+
14
+ class KonectyDateTimeError(Exception):
15
+ """Exceção base para erros de data/hora."""
16
+
17
+ pass
18
+
19
+
20
+ class KonectyDateTimeFormatError(KonectyDateTimeError):
21
+ """Exceção para erros de formato de data/hora."""
22
+
23
+ def __init__(self):
24
+ super().__init__("Data em formato inválido")
25
+
26
+
27
+ class KonectyDateTimeTypeError(KonectyDateTimeError):
28
+ """Exceção para erros de tipo de data/hora."""
29
+
30
+ def __init__(self):
31
+ super().__init__("Tipo inválido para KonectyDateTime")
32
+
33
+
34
+ class KonectyDateTime(datetime):
35
+ """Classe personalizada para manipular datetime com o formato {'$date': 'ISO8601 string'}."""
36
+
37
+ @classmethod
38
+ def from_datetime(cls, dt: datetime) -> Self:
39
+ return cls(
40
+ dt.year,
41
+ dt.month,
42
+ dt.day,
43
+ dt.hour,
44
+ dt.minute,
45
+ dt.second,
46
+ dt.microsecond,
47
+ dt.tzinfo,
48
+ )
49
+
50
+ @classmethod
51
+ def from_json(cls, json: dict[str, Any]) -> Self:
52
+ date_str = json["$date"]
53
+ date = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
54
+ return cls(
55
+ date.year,
56
+ date.month,
57
+ date.day,
58
+ date.hour,
59
+ date.minute,
60
+ date.second,
61
+ date.microsecond,
62
+ date.tzinfo,
63
+ )
64
+
65
+ @classmethod
66
+ def from_any(cls, value: Any) -> Self:
67
+ if isinstance(value, dict) and "$date" in value:
68
+ return cls.from_json(value)
69
+ if isinstance(value, datetime):
70
+ return cls.from_datetime(value)
71
+ if isinstance(value, str):
72
+ return cls.from_isoformat(value)
73
+ raise KonectyDateTimeTypeError
74
+
75
+ @classmethod
76
+ def from_isoformat(cls, value: str) -> Self:
77
+ date = datetime.fromisoformat(value.replace("Z", "+00:00"))
78
+ return cls(
79
+ date.year,
80
+ date.month,
81
+ date.day,
82
+ date.hour,
83
+ date.minute,
84
+ date.second,
85
+ date.microsecond,
86
+ date.tzinfo,
87
+ )
88
+
89
+ @classmethod
90
+ def __get_validators__(cls):
91
+ yield cls.validate
92
+
93
+ @classmethod
94
+ def validate(cls, v):
95
+ if isinstance(v, dict) and "$date" in v:
96
+ try:
97
+ return datetime.fromisoformat(v["$date"].replace("Z", "+00:00"))
98
+ except Exception as e:
99
+ raise KonectyDateTimeFormatError from e
100
+ elif isinstance(v, datetime):
101
+ return v
102
+ raise KonectyDateTimeTypeError
103
+
104
+ @classmethod
105
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> CoreSchema:
106
+ """Define o schema para serialização/deserialização do Pydantic."""
107
+ return core_schema.json_or_python_schema(
108
+ json_schema=core_schema.typed_dict_schema(
109
+ {
110
+ "$date": core_schema.typed_dict_field(core_schema.str_schema()),
111
+ },
112
+ total=True,
113
+ ),
114
+ python_schema=core_schema.datetime_schema(),
115
+ serialization=core_schema.plain_serializer_function_ser_schema(
116
+ lambda x: {"$date": x.isoformat()}
117
+ ),
118
+ )
119
+
120
+ @classmethod
121
+ def __get_pydantic_json_schema__(
122
+ cls, core_schema: Any, handler: Any
123
+ ) -> JsonSchemaValue:
124
+ """Define o schema JSON para documentação."""
125
+ json_schema = handler(core_schema)
126
+ json_schema.update(
127
+ examples=["2023-01-01T00:00:00Z"],
128
+ type="string",
129
+ format="date-time",
130
+ )
131
+ return json_schema
132
+
133
+ def to_json(self):
134
+ return {"$date": self.isoformat()}
135
+
136
+
137
+ class Address(BaseModel):
138
+ """Representa um endereço completo.
139
+
140
+ Esta classe modela informações detalhadas de um endereço, incluindo
141
+ dados geográficos e informações de localização específicas.
142
+ """
143
+
144
+ number: str | None = Field(None, description="Número do endereço.")
145
+ postal_code: str | None = Field(
146
+ None, description="Código postal ou CEP do endereço."
147
+ )
148
+ street: str | None = Field(None, description="Nome da rua, avenida ou logradouro.")
149
+ district: str | None = Field(None, description="Bairro ou distrito.")
150
+ city: str | None = Field(None, description="Cidade.")
151
+ state: str | None = Field(None, description="Estado ou província.")
152
+ place_type: str | None = Field(
153
+ None, description="Tipo de logradouro (por exemplo, Rua, Avenida, Praça)."
154
+ )
155
+ complement: str | None = Field(
156
+ None, description="Informações complementares do endereço."
157
+ )
158
+ country: str | None = Field(None, description="País.")
159
+ geo_location: tuple[float, float] | None = Field(
160
+ None, description="Localização geográfica do endereço."
161
+ )
162
+
163
+ def to_dict(self) -> dict[str, Any]:
164
+ return self.model_dump(by_alias=True)
165
+
166
+
167
+ class KonectyUser(BaseModel):
168
+ """Representa um usuário do Konecty."""
169
+
170
+ id: str = Field(alias="_id")
171
+ name: str = Field(alias="name")
172
+ active: bool = Field(alias="active")
173
+
174
+
175
+ class KonectyBaseModel(BaseModel):
176
+ """Modelo base para documentos do Konecty."""
177
+
178
+ class UserRef(BaseModel):
179
+ id: Optional[str] = Field(None, alias="_id")
180
+ name: Optional[str] = None
181
+ group: Optional[dict] = None
182
+ active: Optional[bool] = None
183
+
184
+ id: Optional[str] = Field(None, alias="_id")
185
+ code: Optional[int] = None
186
+ created_at: Optional[datetime] = Field(None, alias="_createdAt")
187
+ created_by: Optional[UserRef] = Field(None, alias="_createdBy")
188
+ updated_at: Optional[datetime] = Field(None, alias="_updatedAt")
189
+ updated_by: Optional[UserRef] = Field(None, alias="_updatedBy")
190
+ users: Optional[List[UserRef]] = Field(None, alias="_user")
191
+
192
+ model_config = {
193
+ "populate_by_name": True,
194
+ "json_encoders": {
195
+ datetime: lambda v: json.dumps({"$date": v.isoformat()}),
196
+ Decimal: lambda v: str(v),
197
+ },
198
+ "extra": "allow",
199
+ }
200
+
201
+ @classmethod
202
+ def from_json(cls, json_str: str) -> Self:
203
+ """Cria uma instância do modelo a partir de uma string JSON.
204
+
205
+ Args:
206
+ json_str: String JSON válida representando o objeto
207
+
208
+ Returns:
209
+ Uma instância do modelo
210
+
211
+ Raises:
212
+ ValueError: Se o JSON for inválido
213
+ ValidationError: Se os dados não corresponderem ao modelo
214
+ """
215
+ try:
216
+ data = json.loads(json_str)
217
+ return cls.from_dict(data)
218
+ except json.JSONDecodeError as e:
219
+ raise ValueError(f"JSON inválido: {str(e)}")
220
+
221
+ @classmethod
222
+ def from_dict(cls, data: Dict[str, Any]) -> Self:
223
+ """Cria uma instância do modelo a partir de um dicionário.
224
+
225
+ Args:
226
+ data: Dicionário com os dados do objeto
227
+
228
+ Returns:
229
+ Uma instância do modelo
230
+
231
+ Raises:
232
+ ValidationError: Se os dados não corresponderem ao modelo
233
+ """
234
+ return cls.model_validate(data)
235
+
236
+ def to_json(self, **kwargs) -> str:
237
+ """Converte o modelo para uma string JSON.
238
+
239
+ Args:
240
+ **kwargs: Argumentos adicionais para json.dumps
241
+
242
+ Returns:
243
+ String JSON representando o objeto
244
+ """
245
+ return json.dumps(self.to_dict(), **kwargs)
246
+
247
+ def to_dict(self) -> Dict[str, Any]:
248
+ """Converte o modelo para um dicionário.
249
+
250
+ Returns:
251
+ Dicionário representando o objeto
252
+ """
253
+ return self.model_dump(by_alias=True)
254
+
255
+ def to_update_dict(self) -> Dict[str, Any]:
256
+ """Converte o modelo para um dicionário de atualização.
257
+
258
+ Returns:
259
+ Dicionário representando o objeto
260
+ """
261
+ return {"_id": self.id, "_updatedAt": self.updated_at or datetime.now()}
262
+
263
+ def extend(self, data: Dict[str, Any]):
264
+ """Extende esse objeto, adcionando os dados recebidos"""
265
+ field_names = list(self.model_fields.keys())
266
+
267
+ for key, value in data.items():
268
+ if key in field_names:
269
+ setattr(self, key, value)
270
+
271
+
272
+ class KonectyLabel(BaseModel):
273
+ pt_br: str = Field(alias="pt_BR")
274
+ en: str = Field(alias="en")
275
+
276
+
277
+ class KonectyPhone(BaseModel):
278
+ country_code: Optional[int] = Field(None, ge=1, le=999, alias="countryCode")
279
+ phone_number: Optional[str] = Field(
280
+ None, max_length=11, min_length=8, alias="phoneNumber"
281
+ )
282
+ type: Optional[str] = Field(
283
+ None,
284
+ description="Tipo do telefone (por exemplo, celular, fixo, comercial)",
285
+ alias="type",
286
+ )
287
+
288
+ model_config = {
289
+ "populate_by_name": True,
290
+ "extra": "allow",
291
+ }
292
+
293
+ @classmethod
294
+ def empty(cls) -> Self:
295
+ return cls(countryCode=None, phoneNumber=None, type=None)
296
+
297
+ @classmethod
298
+ def from_dict(cls, data: dict[str, Any]) -> Self:
299
+ return cls.model_validate(data)
300
+
301
+ @classmethod
302
+ def from_string(cls, value: str) -> Self:
303
+ return cls(
304
+ phoneNumber=re.sub(r"\D", "", value),
305
+ countryCode=55,
306
+ type="mobile",
307
+ )
308
+
309
+ @classmethod
310
+ def from_any(cls, value: Any) -> Self | None:
311
+ if value is None:
312
+ return None
313
+ if isinstance(value, cls):
314
+ return value
315
+ if isinstance(value, str):
316
+ return cls.from_string(value)
317
+ if isinstance(value, dict):
318
+ return cls.from_dict(value)
319
+ raise ValueError("Invalid value for KonectyPhone")
320
+
321
+ def to_dict(self) -> dict[str, Any]:
322
+ return self.model_dump(by_alias=True)
323
+
324
+
325
+ class KonectyLookup(BaseModel):
326
+ """Representa uma referência a outro documento no Konecty."""
327
+
328
+ id: str = Field(alias="_id")
329
+
330
+
331
+ class KonectyEmail(BaseModel):
332
+ """Representa um endereço de e-mail."""
333
+
334
+ address: Optional[str] = Field(None, description="Endereço de e-mail válido")
335
+
336
+ model_config = {
337
+ "populate_by_name": True,
338
+ "extra": "allow",
339
+ }
340
+
341
+ @classmethod
342
+ def empty(cls) -> Self:
343
+ return cls(address=None)
344
+
345
+ @classmethod
346
+ def from_dict(cls, data: dict[str, Any]) -> Self:
347
+ return cls.model_validate(data)
348
+
349
+ @classmethod
350
+ def from_string(cls, value: str) -> Self:
351
+ return cls(address=value)
352
+
353
+ @classmethod
354
+ def from_any(cls, value: Any) -> Self | None:
355
+ if value is None:
356
+ return None
357
+ if isinstance(value, str):
358
+ return cls.from_string(value)
359
+ if isinstance(value, dict):
360
+ return cls.from_dict(value)
361
+ if isinstance(value, cls):
362
+ return value
363
+ raise ValueError("Invalid value for KonectyEmail")
364
+
365
+ def to_dict(self) -> dict[str, Any]:
366
+ return self.model_dump(by_alias=True)
367
+
368
+
369
+ class KonectyPersonName(BaseModel):
370
+ """Representa um nome completo de pessoa."""
371
+
372
+ first: str | None = Field(None, description="Primeiro nome")
373
+ last: str | None = Field(None, description="Sobrenome")
374
+ full: str = Field(description="Nome completo")
375
+
376
+ @classmethod
377
+ def from_dict(cls, data: dict[str, Any]) -> Self:
378
+ return cls.model_validate(data)
379
+
380
+ @classmethod
381
+ def from_string(cls, value: str) -> Self:
382
+ name = value.split(" ")
383
+ return cls(first=name[0], last=" ".join(name[1:]), full=value)
384
+
385
+ @classmethod
386
+ def from_any(cls, value: Any) -> Self:
387
+ if isinstance(value, str):
388
+ return cls.from_string(value)
389
+ if isinstance(value, dict):
390
+ return cls.from_dict(value)
391
+ if isinstance(value, cls):
392
+ return value
393
+ raise ValueError("Invalid value for KonectyPersonName")
394
+
395
+ def to_dict(self) -> dict[str, Any]:
396
+ return self.model_dump(by_alias=True)
397
+
398
+
399
+ class KonectyUpdateId(BaseModel):
400
+ id: str = Field(alias="_id")
401
+ updatedAt: KonectyDateTime = Field(alias="_updatedAt")
402
+
403
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
404
+
405
+ @classmethod
406
+ def from_dict(cls, data: dict[str, Any]) -> Self:
407
+ id = data.get("_id", data.get("id", None))
408
+ updatedAt = data.get("_updatedAt", data.get("updatedAt", None))
409
+ if id is None or updatedAt is None:
410
+ raise ValueError("Invalid value for KonectyUpdateIds")
411
+ return cls(id=id, updatedAt=KonectyDateTime.from_any(updatedAt))
412
+
413
+ @classmethod
414
+ def from_list(cls, data: list[dict[str, Any]]) -> list[Self]:
415
+ return [cls.from_dict(item) for item in data]
416
+
417
+ def to_dict(self) -> dict[str, Any]:
418
+ return {"_id": self.id, "_updatedAt": self.updatedAt.to_json()}