konecty_sdk_python 2.1.1__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 (35) hide show
  1. konecty_sdk_python-2.1.1/.gitignore +13 -0
  2. konecty_sdk_python-2.1.1/KonectySdkPython/__init__.py +41 -0
  3. konecty_sdk_python-2.1.1/KonectySdkPython/cli/__init__.py +38 -0
  4. konecty_sdk_python-2.1.1/KonectySdkPython/cli/apply.py +555 -0
  5. konecty_sdk_python-2.1.1/KonectySdkPython/cli/backup.py +161 -0
  6. konecty_sdk_python-2.1.1/KonectySdkPython/cli/pull.py +309 -0
  7. konecty_sdk_python-2.1.1/KonectySdkPython/lib/__init__.py +43 -0
  8. konecty_sdk_python-2.1.1/KonectySdkPython/lib/client.py +950 -0
  9. konecty_sdk_python-2.1.1/KonectySdkPython/lib/exceptions.py +26 -0
  10. konecty_sdk_python-2.1.1/KonectySdkPython/lib/feature_types/__init__.py +46 -0
  11. konecty_sdk_python-2.1.1/KonectySdkPython/lib/feature_types/cross_module_query.py +78 -0
  12. konecty_sdk_python-2.1.1/KonectySdkPython/lib/feature_types/kpi.py +21 -0
  13. konecty_sdk_python-2.1.1/KonectySdkPython/lib/feature_types/query_json.py +242 -0
  14. konecty_sdk_python-2.1.1/KonectySdkPython/lib/file_manager.py +248 -0
  15. konecty_sdk_python-2.1.1/KonectySdkPython/lib/filters.py +155 -0
  16. konecty_sdk_python-2.1.1/KonectySdkPython/lib/http.py +95 -0
  17. konecty_sdk_python-2.1.1/KonectySdkPython/lib/model.py +88 -0
  18. konecty_sdk_python-2.1.1/KonectySdkPython/lib/serialization.py +13 -0
  19. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/__init__.py +5 -0
  20. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/aggregation.py +142 -0
  21. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/auth.py +105 -0
  22. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/base.py +63 -0
  23. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/change_user.py +64 -0
  24. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/comments.py +89 -0
  25. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/export.py +54 -0
  26. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/files.py +59 -0
  27. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/notifications.py +41 -0
  28. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/query.py +192 -0
  29. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/stream.py +133 -0
  30. konecty_sdk_python-2.1.1/KonectySdkPython/lib/services/subscriptions.py +26 -0
  31. konecty_sdk_python-2.1.1/KonectySdkPython/lib/settings.py +152 -0
  32. konecty_sdk_python-2.1.1/KonectySdkPython/lib/types.py +418 -0
  33. konecty_sdk_python-2.1.1/PKG-INFO +155 -0
  34. konecty_sdk_python-2.1.1/README.md +125 -0
  35. konecty_sdk_python-2.1.1/pyproject.toml +58 -0
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # PyPI
13
+ .pypirc
@@ -0,0 +1,41 @@
1
+ """Konecty metadata management package."""
2
+
3
+ from .cli import apply_command, backup_command, pull_command
4
+ from .lib.client import KonectyClient, KonectyDict, KonectyFilter, KonectyFindParams
5
+ from .lib.file_manager import FileManager
6
+ from .lib.model import KonectyModelGenerator
7
+ from .lib.settings import fill_settings, fill_settings_sync
8
+ from .lib.types import Address as KonectyAddress
9
+ from .lib.types import (
10
+ KonectyBaseModel,
11
+ KonectyDateTime,
12
+ KonectyEmail,
13
+ KonectyLabel,
14
+ KonectyLookup,
15
+ KonectyPersonName,
16
+ KonectyPhone,
17
+ KonectyUser,
18
+ )
19
+
20
+ __all__ = [
21
+ "apply_command",
22
+ "backup_command",
23
+ "pull_command",
24
+ "KonectyClient",
25
+ "KonectyDict",
26
+ "KonectyFilter",
27
+ "KonectyFindParams",
28
+ "FileManager",
29
+ "KonectyModelGenerator",
30
+ "KonectyDateTime",
31
+ "KonectyUser",
32
+ "KonectyBaseModel",
33
+ "KonectyLabel",
34
+ "KonectyPhone",
35
+ "KonectyLookup",
36
+ "KonectyEmail",
37
+ "KonectyPersonName",
38
+ "KonectyAddress",
39
+ "fill_settings",
40
+ "fill_settings_sync",
41
+ ]
@@ -0,0 +1,38 @@
1
+ """CLI tools for Konecty metadata management."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+ console = Console()
7
+
8
+
9
+ @click.group()
10
+ def cli():
11
+ """Konecty CLI tools for metadata management."""
12
+ pass
13
+
14
+
15
+ # Import commands after cli group is defined to avoid circular imports
16
+ from .apply import apply_command
17
+ from .backup import backup_command
18
+ from .pull import pull_command
19
+
20
+ # Add commands to the group
21
+ cli.add_command(apply_command)
22
+ cli.add_command(backup_command)
23
+ cli.add_command(pull_command)
24
+
25
+
26
+ def main():
27
+ """Entry point for the CLI."""
28
+ import asyncio
29
+ import sys
30
+
31
+ if sys.platform == "win32":
32
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
33
+
34
+ asyncio.run(cli())
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()
@@ -0,0 +1,555 @@
1
+ # /// script
2
+ # requires-python = ">=3.12"
3
+ # dependencies = [
4
+ # "click",
5
+ # "inquirer",
6
+ # "pymongo",
7
+ # "rich",
8
+ # ]
9
+ # ///
10
+ """Script para aplicar alterações locais ao MongoDB."""
11
+
12
+ import asyncio
13
+ import json
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Literal, Optional, Set, Tuple, TypedDict, cast
16
+
17
+ import click
18
+ import inquirer
19
+ from pymongo import MongoClient
20
+ from rich.console import Console
21
+ from rich.progress import Progress
22
+ from rich.table import Table
23
+
24
+
25
+ class DocFiles(TypedDict):
26
+ document: List[Path]
27
+ view: List[Path]
28
+ list: List[Path]
29
+ pivot: List[Path]
30
+ access: List[Path]
31
+ hook: List[Path]
32
+
33
+
34
+ DocType = Literal["document", "view", "list", "pivot", "access", "hook"]
35
+ MetaType = Literal["view", "list", "pivot", "access"]
36
+
37
+
38
+ console = Console()
39
+
40
+
41
+ def load_json_file(file_path: Path) -> dict:
42
+ """Carrega um arquivo JSON."""
43
+ try:
44
+ return json.loads(file_path.read_text())
45
+ except Exception as error:
46
+ console.print(f"[red]Erro ao ler arquivo {file_path}[/red]: {str(error)}")
47
+ return {}
48
+
49
+
50
+ def find_metadata_files(metadata_dir: Path) -> Dict[str, DocFiles]:
51
+ """Encontra todos os arquivos de metadados no diretório."""
52
+ result: Dict[str, DocFiles] = {}
53
+
54
+ for doc_dir in metadata_dir.iterdir():
55
+ if not doc_dir.is_dir():
56
+ continue
57
+
58
+ doc_files: DocFiles = {
59
+ "document": [],
60
+ "view": [],
61
+ "list": [],
62
+ "pivot": [],
63
+ "access": [],
64
+ "hook": [],
65
+ }
66
+
67
+ # Documento principal
68
+ doc_file = doc_dir / "document.json"
69
+ if doc_file.exists():
70
+ doc_files["document"].append(doc_file)
71
+
72
+ # Views, lists, pivots e access
73
+ for type_name in ("view", "list", "pivot", "access"):
74
+ type_name_key = cast(MetaType, type_name)
75
+ type_dir = doc_dir / type_name
76
+ if type_dir.exists():
77
+ doc_files[type_name_key].extend(sorted(type_dir.glob("*.json")))
78
+
79
+ # Hooks
80
+ hook_dir = doc_dir / "hook"
81
+ if hook_dir.exists():
82
+ doc_files["hook"].extend(sorted(hook_dir.glob("*.*")))
83
+
84
+ if any(files for files in doc_files.values()):
85
+ result[doc_dir.name] = doc_files
86
+
87
+ return result
88
+
89
+
90
+ def is_equal_documents(doc1: dict, doc2: dict) -> bool:
91
+ """Compara dois documentos ignorando campos específicos."""
92
+ # Campos para ignorar na comparação
93
+ ignore_fields = ["_id", "_createdAt", "_updatedAt", "_createdBy", "_updatedBy"]
94
+
95
+ # Cria cópias para não modificar os originais
96
+ doc1_copy = doc1.copy()
97
+ doc2_copy = doc2.copy()
98
+
99
+ # Remove campos ignorados
100
+ for field in ignore_fields:
101
+ doc1_copy.pop(field, None)
102
+ doc2_copy.pop(field, None)
103
+
104
+ return doc1_copy == doc2_copy
105
+
106
+
107
+ async def apply_document(
108
+ collection,
109
+ doc_name: str,
110
+ doc_files: DocFiles,
111
+ dry_run: bool = False,
112
+ ) -> Tuple[List[str], List[str], List[str]]:
113
+ """Aplica as alterações de um documento."""
114
+ applied = []
115
+ errors = []
116
+ skipped = []
117
+
118
+ # Documento principal
119
+ if doc_files["document"]:
120
+ doc_data = load_json_file(doc_files["document"][0])
121
+ if doc_data:
122
+ existing_doc = collection.find_one(
123
+ {"_id": doc_data["_id"], "type": {"$in": ["composite", "document"]}}
124
+ )
125
+
126
+ if existing_doc and is_equal_documents(doc_data, existing_doc):
127
+ skipped.append(f"⚡ {doc_name} (document) [identical]")
128
+ else:
129
+ if not dry_run:
130
+ try:
131
+ collection.update_one(
132
+ {
133
+ "_id": doc_data["_id"],
134
+ "type": {"$in": ["composite", "document"]},
135
+ },
136
+ {"$set": doc_data},
137
+ upsert=True,
138
+ )
139
+ applied.append(f"✓ {doc_name} (document)")
140
+ except Exception as error:
141
+ print(doc_data["_id"], f"{doc_name} (document)")
142
+ print(error)
143
+ errors.append(f"✗ {doc_name} (document): {str(error)}")
144
+ else:
145
+ applied.append(f"✓ {doc_name} (document) [dry-run]")
146
+
147
+ # Views, lists, pivots e access
148
+ existing_data: dict[str, Any] | None = None
149
+ data: Any
150
+
151
+ for type_name in ("view", "list", "pivot", "access"):
152
+ type_name_key = cast(MetaType, type_name)
153
+ for file_path in doc_files[type_name_key]:
154
+ data = load_json_file(file_path)
155
+ if data:
156
+ existing_data = collection.find_one(
157
+ {"name": data["name"], "type": type_name, "document": doc_name}
158
+ )
159
+
160
+ if existing_data and is_equal_documents(data, existing_data):
161
+ skipped.append(
162
+ f"⚡ {doc_name}/{type_name}/{data['name']} [identical]"
163
+ )
164
+ else:
165
+ if not dry_run:
166
+ try:
167
+ collection.update_one(
168
+ {
169
+ "name": data["name"],
170
+ "type": type_name,
171
+ "document": doc_name,
172
+ },
173
+ {"$set": data},
174
+ upsert=True,
175
+ )
176
+ applied.append(f"✓ {doc_name}/{type_name}/{data['name']}")
177
+ except Exception as error:
178
+ print(
179
+ data["_id"],
180
+ data["name"],
181
+ f"{doc_name}/{type_name}/{data['name']}",
182
+ )
183
+ print(error)
184
+ errors.append(
185
+ f"✗ {doc_name}/{type_name}/{data['name']}: {str(error)}"
186
+ )
187
+ else:
188
+ applied.append(
189
+ f"✓ {doc_name}/{type_name}/{data['name']} [dry-run]"
190
+ )
191
+
192
+ # Hooks
193
+ for file_path in doc_files["hook"]:
194
+ data = file_path.read_text()
195
+ if data:
196
+ existing_data = collection.find_one(
197
+ {"name": doc_name, "type": {"$in": ["composite", "document"]}}
198
+ )
199
+ hook_name = file_path.stem
200
+ if existing_data:
201
+ if existing_data.get(hook_name, None) == data:
202
+ skipped.append(f"⚡ {doc_name}/{hook_name} [identical]")
203
+ else:
204
+ if not dry_run:
205
+ try:
206
+ collection.update_one(
207
+ {
208
+ "name": doc_name,
209
+ "type": {"$in": ["composite", "document"]},
210
+ },
211
+ {"$set": {hook_name: data}},
212
+ )
213
+ applied.append(f"✓ {doc_name}/{hook_name}")
214
+ except Exception as error:
215
+ errors.append(f"✗ {doc_name}/{hook_name}: {str(error)}")
216
+ else:
217
+ applied.append(f"✓ {doc_name}/{hook_name} [dry-run]")
218
+
219
+ return applied, errors, skipped
220
+
221
+
222
+ async def prune_documents(
223
+ collection,
224
+ local_docs: Set[str],
225
+ dry_run: bool = False,
226
+ ) -> Tuple[List[str], List[str]]:
227
+ """Remove documentos que não existem localmente."""
228
+ pruned = []
229
+ errors = []
230
+
231
+ # Encontra documentos no banco que não existem localmente
232
+ remote_docs = set(
233
+ doc["name"]
234
+ for doc in collection.find(
235
+ {"type": {"$in": ["composite", "document"]}, "name": {"$ne": "_id"}},
236
+ {"name": 1},
237
+ )
238
+ )
239
+
240
+ to_remove = remote_docs - local_docs
241
+
242
+ for doc_name in to_remove:
243
+ if not dry_run:
244
+ try:
245
+ # Remove documento principal
246
+ collection.delete_one(
247
+ {"name": doc_name, "type": {"$in": ["composite", "document"]}}
248
+ )
249
+ # Remove documentos relacionados
250
+ collection.delete_many({"document": doc_name})
251
+ pruned.append(f"✓ {doc_name}")
252
+ except Exception as error:
253
+ errors.append(f"✗ {doc_name}: {str(error)}")
254
+ else:
255
+ pruned.append(f"✓ {doc_name} [dry-run]")
256
+
257
+ return pruned, errors
258
+
259
+
260
+ async def apply_namespace(
261
+ collection,
262
+ metadata_dir: Path,
263
+ dry_run: bool = False,
264
+ ) -> Tuple[List[str], List[str], List[str]]:
265
+ """Aplica as alterações do Namespace."""
266
+ applied = []
267
+ errors = []
268
+ skipped = []
269
+
270
+ base_namespace = {
271
+ "_id": "Namespace",
272
+ "name": "konecty",
273
+ "type": "namespace",
274
+ "ns": "konecty",
275
+ "active": True,
276
+ }
277
+
278
+ namespace_file = metadata_dir / "Namespace.json"
279
+ if not namespace_file.exists():
280
+ namespace_data = base_namespace
281
+ else:
282
+ namespace_data = load_json_file(namespace_file)
283
+ if not namespace_data:
284
+ return [], ["✗ Namespace.json: Arquivo vazio ou inválido"], []
285
+
286
+ existing_namespace = collection.find_one({"_id": "Namespace"})
287
+
288
+ def compare_namespace(ns1: dict, ns2: dict) -> bool:
289
+ """Compara dois namespaces ignorando _id e name."""
290
+ ns1_copy = ns1.copy()
291
+ ns2_copy = ns2.copy()
292
+
293
+ ignore_fields = ["_id", "name", "ns"]
294
+ for field in ignore_fields:
295
+ ns1_copy.pop(field, None)
296
+ ns2_copy.pop(field, None)
297
+
298
+ return ns1_copy == ns2_copy
299
+
300
+ if existing_namespace and compare_namespace(namespace_data, existing_namespace):
301
+ skipped.append("⚡ Namespace.json [identical]")
302
+ else:
303
+ if not dry_run:
304
+ try:
305
+ if existing_namespace:
306
+ collection.update_one(
307
+ {"_id": "Namespace"}, {"$set": namespace_data}
308
+ )
309
+ else:
310
+ collection.insert_one({**base_namespace, **namespace_data})
311
+ applied.append("✓ Namespace.json")
312
+ except Exception as error:
313
+ errors.append(f"✗ Namespace.json: {str(error)}")
314
+ else:
315
+ applied.append("✓ Namespace.json [dry-run]")
316
+
317
+ return applied, errors, skipped
318
+
319
+
320
+ @click.command(name="apply")
321
+ @click.option("--metadata-dir", default="metadata", help="Diretório com os metadados")
322
+ @click.option("--host", default="localhost", help="Host do MongoDB")
323
+ @click.option("--port", type=int, default=27017, help="Porta do MongoDB")
324
+ @click.option("--database", default="default", help="Nome do banco de dados")
325
+ @click.option("--username", help="Usuário do MongoDB")
326
+ @click.option("--password", help="Senha do MongoDB")
327
+ @click.option("--replicaset", help="Nome do ReplicaSet do MongoDB (ex: rs0)")
328
+ @click.option("--document", help="Nome do documento para aplicar")
329
+ @click.option(
330
+ "--prune", is_flag=True, help="Remove documentos que não existem localmente"
331
+ )
332
+ @click.option("--dry-run", is_flag=True, help="Simula as alterações sem aplicá-las")
333
+ @click.option("--direct-connection", is_flag=True, help="Usa conexão direta ao MongoDB")
334
+ @click.option(
335
+ "--retry-writes",
336
+ is_flag=True,
337
+ default=True,
338
+ help="Tenta reescrever em caso de falha",
339
+ )
340
+ @click.option("--w", default="majority", help="Nível de escrita do MongoDB")
341
+ @click.option("--mongo-url", help="URL do MongoDB")
342
+ def apply_command(
343
+ metadata_dir: str,
344
+ host: str,
345
+ port: int,
346
+ database: str,
347
+ username: Optional[str],
348
+ password: Optional[str],
349
+ replicaset: Optional[str],
350
+ document: Optional[str],
351
+ prune: bool,
352
+ dry_run: bool,
353
+ direct_connection: bool,
354
+ retry_writes: bool,
355
+ w: str,
356
+ mongo_url: Optional[str],
357
+ ) -> None:
358
+ """Aplica alterações locais ao MongoDB."""
359
+ asyncio.run(
360
+ _apply_command(
361
+ metadata_dir=metadata_dir,
362
+ host=host,
363
+ port=port,
364
+ database=database,
365
+ username=username,
366
+ password=password,
367
+ replicaset=replicaset,
368
+ document=document,
369
+ prune=prune,
370
+ dry_run=dry_run,
371
+ direct_connection=direct_connection,
372
+ retry_writes=retry_writes,
373
+ w=w,
374
+ mongo_url=mongo_url,
375
+ )
376
+ )
377
+
378
+
379
+ async def _apply_command(
380
+ metadata_dir: str,
381
+ host: str,
382
+ port: int,
383
+ database: str,
384
+ username: Optional[str],
385
+ password: Optional[str],
386
+ replicaset: Optional[str],
387
+ document: Optional[str],
388
+ prune: bool,
389
+ dry_run: bool,
390
+ direct_connection: bool,
391
+ retry_writes: bool,
392
+ w: str,
393
+ mongo_url: Optional[str],
394
+ ) -> None:
395
+ """Internal async implementation of apply command."""
396
+ metadata_path = Path(metadata_dir).resolve()
397
+ if not metadata_path.exists():
398
+ console.print(f"[red]Diretório {metadata_dir} não encontrado[/red]")
399
+ return
400
+
401
+ uri = mongo_url
402
+
403
+ if not uri:
404
+ uri_params = []
405
+ if replicaset:
406
+ uri_params.extend(
407
+ [
408
+ f"replicaSet={replicaset}",
409
+ f"directConnection={'true' if direct_connection else 'false'}",
410
+ f"retryWrites={'true' if retry_writes else 'false'}",
411
+ f"w={w}",
412
+ ]
413
+ )
414
+
415
+ uri_suffix = f"?{'&'.join(uri_params)}" if uri_params else ""
416
+
417
+ if username and password:
418
+ uri = f"mongodb://{username}:{password}@{host}:{port}/admin{uri_suffix}"
419
+ else:
420
+ uri = f"mongodb://{host}:{port}{uri_suffix}"
421
+
422
+ client = MongoClient(
423
+ uri,
424
+ serverSelectionTimeoutMS=30000,
425
+ connectTimeoutMS=20000,
426
+ socketTimeoutMS=20000,
427
+ maxPoolSize=1,
428
+ )
429
+ db = client[database]
430
+ collection = db["MetaObjects"]
431
+
432
+ # Se o documento for Namespace, aplica apenas o Namespace
433
+ if document == "Namespace":
434
+ namespace_applied, namespace_errors, namespace_skipped = await apply_namespace(
435
+ collection, metadata_path, dry_run
436
+ )
437
+ table = Table(title="Resultados da Aplicação")
438
+ table.add_column("Documento")
439
+ table.add_column("Status")
440
+ table.add_column("Pulados")
441
+ table.add_column("Erros")
442
+ if namespace_applied or namespace_errors or namespace_skipped:
443
+ table.add_row(
444
+ "@Namespace",
445
+ "\n".join(namespace_applied),
446
+ "\n".join(namespace_skipped),
447
+ "\n".join(namespace_errors),
448
+ )
449
+ client.close()
450
+ console.print(table)
451
+ return
452
+
453
+ # Encontra todos os arquivos de metadados
454
+ all_files = find_metadata_files(metadata_path)
455
+
456
+ if not all_files:
457
+ console.print("[yellow]Nenhum arquivo de metadados encontrado[/yellow]")
458
+ return
459
+
460
+ # Se nenhum documento específico foi fornecido, pergunta ao usuário
461
+ if document is None:
462
+ choices = [
463
+ {"name": "Todos", "value": "all"},
464
+ *[{"name": name, "value": name} for name in sorted(all_files.keys())],
465
+ ]
466
+
467
+ questions = [
468
+ inquirer.List(
469
+ "document",
470
+ message="Qual documento você deseja aplicar?",
471
+ choices=[choice["name"] for choice in choices],
472
+ )
473
+ ]
474
+
475
+ answers = inquirer.prompt(questions)
476
+ if answers is None:
477
+ console.print("[red]Operação cancelada pelo usuário[/red]")
478
+ return
479
+
480
+ document = "all" if answers["document"] == "Todos" else answers["document"]
481
+
482
+ # Determina quais documentos processar
483
+ docs_to_process = list(all_files.keys()) if document == "all" else [document]
484
+
485
+ if document != "all" and document not in all_files:
486
+ console.print(f"[red]Documento {document} não encontrado[/red]")
487
+ return
488
+
489
+ # Aplica o Namespace primeiro
490
+ namespace_applied, namespace_errors, namespace_skipped = await apply_namespace(
491
+ collection, metadata_path, dry_run
492
+ )
493
+
494
+ table = Table(title="Resultados da Aplicação")
495
+ table.add_column("Documento")
496
+ table.add_column("Status")
497
+ table.add_column("Pulados")
498
+ table.add_column("Erros")
499
+
500
+ if namespace_applied or namespace_errors or namespace_skipped:
501
+ table.add_row(
502
+ "@Namespace",
503
+ "\n".join(namespace_applied),
504
+ "\n".join(namespace_skipped),
505
+ "\n".join(namespace_errors),
506
+ )
507
+
508
+ with Progress() as progress:
509
+ task = progress.add_task(
510
+ "[cyan]Aplicando alterações...", total=len(docs_to_process)
511
+ )
512
+
513
+ for doc_name in docs_to_process:
514
+ applied, errors, skipped = await apply_document(
515
+ collection, doc_name, all_files[doc_name], dry_run
516
+ )
517
+
518
+ table.add_row(
519
+ doc_name,
520
+ "\n".join(applied),
521
+ "\n".join(skipped),
522
+ "\n".join(errors) if errors else "",
523
+ )
524
+
525
+ progress.update(task, advance=1)
526
+
527
+ if prune:
528
+ pruned, prune_errors = await prune_documents(
529
+ collection, set(all_files.keys()), dry_run
530
+ )
531
+ if pruned or prune_errors:
532
+ table.add_row(
533
+ "Prune",
534
+ "\n".join(pruned),
535
+ "",
536
+ "\n".join(prune_errors) if prune_errors else "",
537
+ )
538
+
539
+ client.close()
540
+ console.print(table)
541
+
542
+
543
+ def main():
544
+ """Entry point for the CLI."""
545
+ import asyncio
546
+ import sys
547
+
548
+ if sys.platform == "win32":
549
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
550
+
551
+ apply_command()
552
+
553
+
554
+ if __name__ == "__main__":
555
+ main()