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.
- KonectySdkPython/__init__.py +41 -0
- KonectySdkPython/cli/__init__.py +38 -0
- KonectySdkPython/cli/apply.py +555 -0
- KonectySdkPython/cli/backup.py +161 -0
- KonectySdkPython/cli/pull.py +309 -0
- KonectySdkPython/lib/__init__.py +43 -0
- KonectySdkPython/lib/client.py +950 -0
- KonectySdkPython/lib/exceptions.py +26 -0
- KonectySdkPython/lib/feature_types/__init__.py +46 -0
- KonectySdkPython/lib/feature_types/cross_module_query.py +78 -0
- KonectySdkPython/lib/feature_types/kpi.py +21 -0
- KonectySdkPython/lib/feature_types/query_json.py +242 -0
- KonectySdkPython/lib/file_manager.py +248 -0
- KonectySdkPython/lib/filters.py +155 -0
- KonectySdkPython/lib/http.py +95 -0
- KonectySdkPython/lib/model.py +88 -0
- KonectySdkPython/lib/serialization.py +13 -0
- KonectySdkPython/lib/services/__init__.py +5 -0
- KonectySdkPython/lib/services/aggregation.py +142 -0
- KonectySdkPython/lib/services/auth.py +105 -0
- KonectySdkPython/lib/services/base.py +63 -0
- KonectySdkPython/lib/services/change_user.py +64 -0
- KonectySdkPython/lib/services/comments.py +89 -0
- KonectySdkPython/lib/services/export.py +54 -0
- KonectySdkPython/lib/services/files.py +59 -0
- KonectySdkPython/lib/services/notifications.py +41 -0
- KonectySdkPython/lib/services/query.py +192 -0
- KonectySdkPython/lib/services/stream.py +133 -0
- KonectySdkPython/lib/services/subscriptions.py +26 -0
- KonectySdkPython/lib/settings.py +152 -0
- KonectySdkPython/lib/types.py +418 -0
- konecty_sdk_python-2.1.1.dist-info/METADATA +155 -0
- konecty_sdk_python-2.1.1.dist-info/RECORD +35 -0
- konecty_sdk_python-2.1.1.dist-info/WHEEL +4 -0
- konecty_sdk_python-2.1.1.dist-info/entry_points.txt +2 -0
|
@@ -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()
|