konecty-sdk-python 0.1.0__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.
lib/filters.py ADDED
@@ -0,0 +1,155 @@
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
lib/model.py ADDED
@@ -0,0 +1,76 @@
1
+ from typing import Any, Dict, List, Optional, Type, TypeVar, cast
2
+
3
+ from pydantic import BaseModel, Field, create_model
4
+ from pydantic.fields import FieldInfo
5
+
6
+ from konecty.types import Address, KonectyDateTime, KonectyEmail, KonectyLookup, KonectyPersonName, KonectyPhone
7
+
8
+ T = TypeVar("T")
9
+ ModelType = TypeVar("ModelType", bound=BaseModel)
10
+
11
+
12
+ class KonectyModelGenerator:
13
+ def __init__(self, schema: Dict[str, Any]):
14
+ self.schema = schema
15
+ self.type_mapping: Dict[str, Type[Any]] = {
16
+ "json": dict,
17
+ "richText": str,
18
+ "lookup": KonectyLookup,
19
+ "picklist": str,
20
+ "url": str,
21
+ "boolean": bool,
22
+ "text": str,
23
+ "dateTime": KonectyDateTime,
24
+ "address": Address,
25
+ "email": KonectyEmail,
26
+ "phone": KonectyPhone,
27
+ "personName": KonectyPersonName,
28
+ }
29
+
30
+ def generate_model(self, language: str = "en") -> Type[BaseModel]:
31
+ fields: Dict[str, tuple[Type[Any], FieldInfo]] = {}
32
+ for field in self.schema["fields"]:
33
+ field_name = field["name"]
34
+ field_raw_type = field["type"]
35
+ description = (
36
+ field["help"].get(language, field["help"].get("en", ""))
37
+ if "help" in field
38
+ else field["label"].get(language, field["label"].get("en", ""))
39
+ )
40
+
41
+ base_type = self._get_field_type(field_raw_type)
42
+ current_type = base_type
43
+
44
+ if field.get("isList"):
45
+ current_type = List[base_type] # type: ignore
46
+
47
+ if field.get("minSelected", 1) > 1 or field.get("maxSelected", 1) > 1:
48
+ current_type = List[base_type] # type: ignore
49
+
50
+ is_required = field.get("isRequired", False) or field.get("minSelected", 0) > 0
51
+
52
+ if not is_required and "defaultValue" not in field:
53
+ current_type = Optional[base_type] # type: ignore
54
+
55
+ field_params = {
56
+ "alias": field_name,
57
+ "description": description,
58
+ }
59
+
60
+ if "defaultValue" in field:
61
+ field_params["default"] = field["defaultValue"]
62
+
63
+ if not is_required and "defaultValue" not in field:
64
+ field_params["default"] = None
65
+
66
+ fields[self._clean_name_for_pydantic(field_name)] = (current_type, Field(**field_params))
67
+
68
+ model_name = self.schema["name"]
69
+ return create_model(model_name, **fields)
70
+
71
+ def _clean_name_for_pydantic(self, name: str) -> str:
72
+ return name.replace("_", "")
73
+
74
+ def _get_field_type(self, field_type: str) -> Type[Any]:
75
+ result = self.type_mapping.get(field_type, Any)
76
+ return cast(Type[Any], result)
lib/settings.py ADDED
@@ -0,0 +1,115 @@
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
+
60
+ for field_name in settings_class.model_fields.keys():
61
+ # Primeiro verifica se existe na variável de ambiente
62
+ env_value = os.getenv(field_name.upper())
63
+ if env_value is not None and env_value.strip():
64
+ field_type = settings_class.model_fields[field_name].annotation
65
+ converted_value = _convert_value(env_value, field_type)
66
+ if converted_value is not None:
67
+ settings_dict[field_name] = converted_value
68
+ continue
69
+ # Se não encontrou na variável de ambiente, busca no Konecty
70
+ if field_name not in settings_dict:
71
+ value = await client.get_setting(field_name)
72
+ if value is not None and value.strip():
73
+ field_type = settings_class.model_fields[field_name].annotation
74
+ converted_value = _convert_value(value, field_type)
75
+ if converted_value is not None:
76
+ settings_dict[field_name] = converted_value
77
+
78
+ return settings_class.model_construct(**settings_dict)
79
+
80
+
81
+ def fill_settings_sync(settings_class: Type[T]) -> T:
82
+ """Versão síncrona de fill_settings.
83
+
84
+ Args:
85
+ settings_class: Classe de configurações que herda de BaseModel.
86
+
87
+ Returns:
88
+ Instância da classe de configurações preenchida com valores do Konecty.
89
+ """
90
+ client = KonectyClient(
91
+ base_url=os.getenv("KONECTY_URL", "http://localhost:3000"),
92
+ token=os.getenv("KONECTY_TOKEN", "invalid_key"),
93
+ )
94
+
95
+ settings_dict = {}
96
+
97
+ for field_name in settings_class.model_fields.keys():
98
+ # Primeiro verifica se existe na variável de ambiente
99
+ env_value = os.getenv(field_name.upper())
100
+ if env_value is not None and env_value.strip():
101
+ field_type = settings_class.model_fields[field_name].annotation
102
+ converted_value = _convert_value(env_value, field_type)
103
+ if converted_value is not None:
104
+ settings_dict[field_name] = converted_value
105
+ continue
106
+ # Se não encontrou na variável de ambiente, busca no Konecty
107
+ if field_name not in settings_dict:
108
+ value = client.get_setting_sync(field_name)
109
+ if value is not None and value.strip():
110
+ field_type = settings_class.model_fields[field_name].annotation
111
+ converted_value = _convert_value(value, field_type)
112
+ if converted_value is not None:
113
+ settings_dict[field_name] = converted_value
114
+
115
+ return settings_class.model_construct(**settings_dict)
lib/types.py ADDED
@@ -0,0 +1,303 @@
1
+ import re
2
+ from datetime import datetime
3
+ from typing import Any, Optional, Self
4
+
5
+ from pydantic import BaseModel, Field
6
+ from pydantic.json_schema import JsonSchemaValue
7
+ from pydantic_core import CoreSchema, core_schema
8
+
9
+
10
+ class KonectyDateTimeError(Exception):
11
+ """Exceção base para erros de data/hora."""
12
+
13
+ pass
14
+
15
+
16
+ class KonectyDateTimeFormatError(KonectyDateTimeError):
17
+ """Exceção para erros de formato de data/hora."""
18
+
19
+ def __init__(self):
20
+ super().__init__("Data em formato inválido")
21
+
22
+
23
+ class KonectyDateTimeTypeError(KonectyDateTimeError):
24
+ """Exceção para erros de tipo de data/hora."""
25
+
26
+ def __init__(self):
27
+ super().__init__("Tipo inválido para KonectyDateTime")
28
+
29
+
30
+ class KonectyDateTime(datetime):
31
+ """Classe personalizada para manipular datetime com o formato {'$date': 'ISO8601 string'}."""
32
+
33
+ @classmethod
34
+ def from_datetime(cls, dt: datetime) -> Self:
35
+ return cls(
36
+ dt.year,
37
+ dt.month,
38
+ dt.day,
39
+ dt.hour,
40
+ dt.minute,
41
+ dt.second,
42
+ dt.microsecond,
43
+ dt.tzinfo,
44
+ )
45
+
46
+ @classmethod
47
+ def from_json(cls, json: dict[str, Any]) -> Self:
48
+ date_str = json["$date"]
49
+ date = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
50
+ return cls(
51
+ date.year,
52
+ date.month,
53
+ date.day,
54
+ date.hour,
55
+ date.minute,
56
+ date.second,
57
+ date.microsecond,
58
+ date.tzinfo,
59
+ )
60
+
61
+ @classmethod
62
+ def from_any(cls, value: Any) -> Self:
63
+ if isinstance(value, dict) and "$date" in value:
64
+ return cls.from_json(value)
65
+ if isinstance(value, datetime):
66
+ return cls.from_datetime(value)
67
+ if isinstance(value, str):
68
+ return cls.from_isoformat(value)
69
+ raise KonectyDateTimeTypeError
70
+
71
+ @classmethod
72
+ def from_isoformat(cls, value: str) -> Self:
73
+ date = datetime.fromisoformat(value.replace("Z", "+00:00"))
74
+ return cls(
75
+ date.year,
76
+ date.month,
77
+ date.day,
78
+ date.hour,
79
+ date.minute,
80
+ date.second,
81
+ date.microsecond,
82
+ date.tzinfo,
83
+ )
84
+
85
+ @classmethod
86
+ def __get_validators__(cls):
87
+ yield cls.validate
88
+
89
+ @classmethod
90
+ def validate(cls, v):
91
+ if isinstance(v, dict) and "$date" in v:
92
+ try:
93
+ return datetime.fromisoformat(v["$date"].replace("Z", "+00:00"))
94
+ except Exception as e:
95
+ raise KonectyDateTimeFormatError from e
96
+ elif isinstance(v, datetime):
97
+ return v
98
+ raise KonectyDateTimeTypeError
99
+
100
+ @classmethod
101
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> CoreSchema:
102
+ """Define o schema para serialização/deserialização do Pydantic."""
103
+ return core_schema.json_or_python_schema(
104
+ json_schema=core_schema.typed_dict_schema(
105
+ {
106
+ "$date": core_schema.typed_dict_field(core_schema.str_schema()),
107
+ },
108
+ total=True,
109
+ ),
110
+ python_schema=core_schema.datetime_schema(),
111
+ serialization=core_schema.plain_serializer_function_ser_schema(
112
+ lambda x: {"$date": x.isoformat()}
113
+ ),
114
+ )
115
+
116
+ @classmethod
117
+ def __get_pydantic_json_schema__(
118
+ cls, core_schema: Any, handler: Any
119
+ ) -> JsonSchemaValue:
120
+ """Define o schema JSON para documentação."""
121
+ json_schema = handler(core_schema)
122
+ json_schema.update(
123
+ examples=["2023-01-01T00:00:00Z"],
124
+ type="string",
125
+ format="date-time",
126
+ )
127
+ return json_schema
128
+
129
+ def to_json(self):
130
+ return {"$date": self.isoformat()}
131
+
132
+
133
+ class Address(BaseModel):
134
+ """Representa um endereço completo.
135
+
136
+ Esta classe modela informações detalhadas de um endereço, incluindo
137
+ dados geográficos e informações de localização específicas.
138
+ """
139
+
140
+ number: str | None = Field(None, description="Número do endereço.")
141
+ postal_code: str | None = Field(
142
+ None, description="Código postal ou CEP do endereço."
143
+ )
144
+ street: str | None = Field(None, description="Nome da rua, avenida ou logradouro.")
145
+ district: str | None = Field(None, description="Bairro ou distrito.")
146
+ city: str | None = Field(None, description="Cidade.")
147
+ state: str | None = Field(None, description="Estado ou província.")
148
+ place_type: str | None = Field(
149
+ None, description="Tipo de logradouro (por exemplo, Rua, Avenida, Praça)."
150
+ )
151
+ complement: str | None = Field(
152
+ None, description="Informações complementares do endereço."
153
+ )
154
+ country: str | None = Field(None, description="País.")
155
+
156
+ def to_dict(self) -> dict[str, Any]:
157
+ return self.model_dump(by_alias=True)
158
+
159
+
160
+ class KonectyUser(BaseModel):
161
+ """Representa um usuário do Konecty."""
162
+
163
+ id: str = Field(alias="_id")
164
+ name: str = Field(alias="name")
165
+ active: bool = Field(alias="active")
166
+
167
+
168
+ class KonectyBaseModel(BaseModel):
169
+ """Modelo base para documentos do Konecty."""
170
+
171
+ id: str = Field(alias="_id")
172
+ created_at: KonectyDateTime = Field(alias="_createdAt")
173
+ created_by: KonectyUser = Field(alias="_createdBy")
174
+ updated_at: KonectyDateTime = Field(alias="_updatedAt")
175
+ updated_by: KonectyUser = Field(alias="_updatedBy")
176
+ user: list[KonectyUser] = Field(alias="_user")
177
+
178
+
179
+ class KonectyLabel(BaseModel):
180
+ pt_br: str = Field(alias="pt_BR")
181
+ en: str = Field(alias="en")
182
+
183
+
184
+ class KonectyPhone(BaseModel):
185
+ country_code: Optional[int] = Field(None, ge=1, le=999, alias="countryCode")
186
+ phone_number: Optional[str] = Field(
187
+ None, max_length=11, min_length=8, alias="phoneNumber"
188
+ )
189
+ type: Optional[str] = Field(
190
+ None,
191
+ description="Tipo do telefone (por exemplo, celular, fixo, comercial)",
192
+ alias="type",
193
+ )
194
+
195
+ model_config = {
196
+ "populate_by_name": True,
197
+ "extra": "allow",
198
+ }
199
+
200
+ @classmethod
201
+ def empty(cls) -> Self:
202
+ return cls(countryCode=None, phoneNumber=None, type=None)
203
+
204
+ @classmethod
205
+ def from_dict(cls, data: dict[str, Any]) -> Self:
206
+ return cls.model_validate(data)
207
+
208
+ @classmethod
209
+ def from_string(cls, value: str) -> Self:
210
+ return cls(
211
+ phoneNumber=re.sub(r"\D", "", value),
212
+ countryCode=55,
213
+ type="mobile",
214
+ )
215
+
216
+ @classmethod
217
+ def from_any(cls, value: Any) -> Self | None:
218
+ if value is None:
219
+ return None
220
+ if isinstance(value, cls):
221
+ return value
222
+ if isinstance(value, str):
223
+ return cls.from_string(value)
224
+ if isinstance(value, dict):
225
+ return cls.from_dict(value)
226
+ raise ValueError("Invalid value for KonectyPhone")
227
+
228
+ def to_dict(self) -> dict[str, Any]:
229
+ return self.model_dump(by_alias=True)
230
+
231
+
232
+ class KonectyLookup(BaseModel):
233
+ """Representa uma referência a outro documento no Konecty."""
234
+
235
+ id: str = Field(alias="_id")
236
+
237
+
238
+ class KonectyEmail(BaseModel):
239
+ """Representa um endereço de e-mail."""
240
+
241
+ address: Optional[str] = Field(None, description="Endereço de e-mail válido")
242
+
243
+ model_config = {
244
+ "populate_by_name": True,
245
+ "extra": "allow",
246
+ }
247
+
248
+ @classmethod
249
+ def empty(cls) -> Self:
250
+ return cls(address=None)
251
+
252
+ @classmethod
253
+ def from_dict(cls, data: dict[str, Any]) -> Self:
254
+ return cls.model_validate(data)
255
+
256
+ @classmethod
257
+ def from_string(cls, value: str) -> Self:
258
+ return cls(address=value)
259
+
260
+ @classmethod
261
+ def from_any(cls, value: Any) -> Self | None:
262
+ if value is None:
263
+ return None
264
+ if isinstance(value, str):
265
+ return cls.from_string(value)
266
+ if isinstance(value, dict):
267
+ return cls.from_dict(value)
268
+ if isinstance(value, cls):
269
+ return value
270
+ raise ValueError("Invalid value for KonectyEmail")
271
+
272
+ def to_dict(self) -> dict[str, Any]:
273
+ return self.model_dump(by_alias=True)
274
+
275
+
276
+ class KonectyPersonName(BaseModel):
277
+ """Representa um nome completo de pessoa."""
278
+
279
+ first: str | None = Field(None, description="Primeiro nome")
280
+ last: str | None = Field(None, description="Sobrenome")
281
+ full: str = Field(description="Nome completo")
282
+
283
+ @classmethod
284
+ def from_dict(cls, data: dict[str, Any]) -> Self:
285
+ return cls.model_validate(data)
286
+
287
+ @classmethod
288
+ def from_string(cls, value: str) -> Self:
289
+ name = value.split(" ")
290
+ return cls(first=name[0], last=" ".join(name[1:]), full=value)
291
+
292
+ @classmethod
293
+ def from_any(cls, value: Any) -> Self:
294
+ if isinstance(value, str):
295
+ return cls.from_string(value)
296
+ if isinstance(value, dict):
297
+ return cls.from_dict(value)
298
+ if isinstance(value, cls):
299
+ return value
300
+ raise ValueError("Invalid value for KonectyPersonName")
301
+
302
+ def to_dict(self) -> dict[str, Any]:
303
+ return self.model_dump(by_alias=True)