d365fo-client 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.
- d365fo_client/__init__.py +305 -0
- d365fo_client/auth.py +93 -0
- d365fo_client/cli.py +700 -0
- d365fo_client/client.py +1454 -0
- d365fo_client/config.py +304 -0
- d365fo_client/crud.py +200 -0
- d365fo_client/exceptions.py +49 -0
- d365fo_client/labels.py +528 -0
- d365fo_client/main.py +502 -0
- d365fo_client/mcp/__init__.py +16 -0
- d365fo_client/mcp/client_manager.py +276 -0
- d365fo_client/mcp/main.py +98 -0
- d365fo_client/mcp/models.py +371 -0
- d365fo_client/mcp/prompts/__init__.py +43 -0
- d365fo_client/mcp/prompts/action_execution.py +480 -0
- d365fo_client/mcp/prompts/sequence_analysis.py +349 -0
- d365fo_client/mcp/resources/__init__.py +15 -0
- d365fo_client/mcp/resources/database_handler.py +555 -0
- d365fo_client/mcp/resources/entity_handler.py +176 -0
- d365fo_client/mcp/resources/environment_handler.py +132 -0
- d365fo_client/mcp/resources/metadata_handler.py +283 -0
- d365fo_client/mcp/resources/query_handler.py +135 -0
- d365fo_client/mcp/server.py +432 -0
- d365fo_client/mcp/tools/__init__.py +17 -0
- d365fo_client/mcp/tools/connection_tools.py +175 -0
- d365fo_client/mcp/tools/crud_tools.py +579 -0
- d365fo_client/mcp/tools/database_tools.py +813 -0
- d365fo_client/mcp/tools/label_tools.py +189 -0
- d365fo_client/mcp/tools/metadata_tools.py +766 -0
- d365fo_client/mcp/tools/profile_tools.py +706 -0
- d365fo_client/metadata_api.py +793 -0
- d365fo_client/metadata_v2/__init__.py +59 -0
- d365fo_client/metadata_v2/cache_v2.py +1372 -0
- d365fo_client/metadata_v2/database_v2.py +585 -0
- d365fo_client/metadata_v2/global_version_manager.py +573 -0
- d365fo_client/metadata_v2/search_engine_v2.py +423 -0
- d365fo_client/metadata_v2/sync_manager_v2.py +819 -0
- d365fo_client/metadata_v2/version_detector.py +439 -0
- d365fo_client/models.py +862 -0
- d365fo_client/output.py +181 -0
- d365fo_client/profile_manager.py +342 -0
- d365fo_client/profiles.py +178 -0
- d365fo_client/query.py +162 -0
- d365fo_client/session.py +60 -0
- d365fo_client/utils.py +196 -0
- d365fo_client-0.1.0.dist-info/METADATA +1084 -0
- d365fo_client-0.1.0.dist-info/RECORD +51 -0
- d365fo_client-0.1.0.dist-info/WHEEL +5 -0
- d365fo_client-0.1.0.dist-info/entry_points.txt +3 -0
- d365fo_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- d365fo_client-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,573 @@
|
|
1
|
+
"""Global version manager for cross-environment metadata sharing."""
|
2
|
+
|
3
|
+
import hashlib
|
4
|
+
import logging
|
5
|
+
from datetime import datetime, timezone
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
7
|
+
|
8
|
+
import aiosqlite
|
9
|
+
|
10
|
+
from ..models import EnvironmentVersionInfo, GlobalVersionInfo, ModuleVersionInfo
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class GlobalVersionManager:
|
16
|
+
"""Manages global version registry and cross-environment sharing"""
|
17
|
+
|
18
|
+
def __init__(self, db_path):
|
19
|
+
"""Initialize global version manager
|
20
|
+
|
21
|
+
Args:
|
22
|
+
db_path: Path to metadata database
|
23
|
+
"""
|
24
|
+
self.db_path = db_path
|
25
|
+
|
26
|
+
async def register_environment_version(
|
27
|
+
self, environment_id: int, modules: List[ModuleVersionInfo]
|
28
|
+
) -> Tuple[int, bool]:
|
29
|
+
"""Register environment version and get/create global version
|
30
|
+
|
31
|
+
Args:
|
32
|
+
environment_id: Environment ID
|
33
|
+
modules: List of module version information
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
Tuple of (global_version_id, is_new_version)
|
37
|
+
"""
|
38
|
+
# Calculate version hashes
|
39
|
+
modules_hash = self._calculate_modules_hash(modules)
|
40
|
+
version_hash = self._calculate_version_hash(modules)
|
41
|
+
|
42
|
+
async with aiosqlite.connect(self.db_path) as db:
|
43
|
+
# Check if this exact version already exists
|
44
|
+
cursor = await db.execute(
|
45
|
+
"SELECT id FROM global_versions WHERE modules_hash = ?", (modules_hash,)
|
46
|
+
)
|
47
|
+
|
48
|
+
existing_version = await cursor.fetchone()
|
49
|
+
if existing_version:
|
50
|
+
global_version_id = existing_version[0]
|
51
|
+
is_new_version = False
|
52
|
+
|
53
|
+
# Update reference count and last used
|
54
|
+
await db.execute(
|
55
|
+
"""UPDATE global_versions
|
56
|
+
SET reference_count = reference_count + 1,
|
57
|
+
last_used_at = CURRENT_TIMESTAMP
|
58
|
+
WHERE id = ?""",
|
59
|
+
(global_version_id,),
|
60
|
+
)
|
61
|
+
|
62
|
+
logger.info(f"Using existing global version {global_version_id}")
|
63
|
+
else:
|
64
|
+
# Create new global version
|
65
|
+
cursor = await db.execute(
|
66
|
+
"""INSERT INTO global_versions
|
67
|
+
(version_hash, modules_hash, created_by_environment_id, reference_count)
|
68
|
+
VALUES (?, ?, ?, 1)""",
|
69
|
+
(version_hash, modules_hash, environment_id),
|
70
|
+
)
|
71
|
+
|
72
|
+
global_version_id = cursor.lastrowid
|
73
|
+
is_new_version = True
|
74
|
+
|
75
|
+
# Store module information
|
76
|
+
await self._store_global_version_modules(db, global_version_id, modules)
|
77
|
+
|
78
|
+
logger.info(f"Created new global version {global_version_id}")
|
79
|
+
|
80
|
+
# Link environment to global version
|
81
|
+
await self._link_environment_to_version(
|
82
|
+
db, environment_id, global_version_id
|
83
|
+
)
|
84
|
+
|
85
|
+
await db.commit()
|
86
|
+
|
87
|
+
return global_version_id, is_new_version
|
88
|
+
|
89
|
+
def _calculate_modules_hash(self, modules: List[ModuleVersionInfo]) -> str:
|
90
|
+
"""Calculate hash of sorted modules
|
91
|
+
|
92
|
+
Args:
|
93
|
+
modules: List of module version information
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
SHA-256 hash of sorted modules
|
97
|
+
"""
|
98
|
+
# Sort modules by ID for consistent hashing
|
99
|
+
sorted_modules = sorted(modules, key=lambda m: m.module_id)
|
100
|
+
|
101
|
+
# Create hash string
|
102
|
+
hash_data = []
|
103
|
+
for module in sorted_modules:
|
104
|
+
hash_data.append(f"{module.module_id}:{module.version}")
|
105
|
+
|
106
|
+
hash_string = "|".join(hash_data)
|
107
|
+
return hashlib.sha256(hash_string.encode()).hexdigest()
|
108
|
+
|
109
|
+
def _calculate_version_hash(self, modules: List[ModuleVersionInfo]) -> str:
|
110
|
+
"""Calculate comprehensive version hash including all module details
|
111
|
+
|
112
|
+
Args:
|
113
|
+
modules: List of module version information
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
SHA-256 hash of all module details
|
117
|
+
"""
|
118
|
+
# Sort modules by ID for consistent hashing
|
119
|
+
sorted_modules = sorted(modules, key=lambda m: m.module_id)
|
120
|
+
|
121
|
+
# Create comprehensive hash string
|
122
|
+
hash_data = []
|
123
|
+
for module in sorted_modules:
|
124
|
+
module_data = [
|
125
|
+
module.module_id,
|
126
|
+
module.version or "",
|
127
|
+
module.name or "",
|
128
|
+
module.publisher or "",
|
129
|
+
module.display_name or "",
|
130
|
+
]
|
131
|
+
hash_data.append(":".join(module_data))
|
132
|
+
|
133
|
+
hash_string = "|".join(hash_data)
|
134
|
+
return hashlib.sha256(hash_string.encode()).hexdigest()
|
135
|
+
|
136
|
+
async def _store_global_version_modules(
|
137
|
+
self,
|
138
|
+
db: aiosqlite.Connection,
|
139
|
+
global_version_id: int,
|
140
|
+
modules: List[ModuleVersionInfo],
|
141
|
+
):
|
142
|
+
"""Store module information for global version
|
143
|
+
|
144
|
+
Args:
|
145
|
+
db: Database connection
|
146
|
+
global_version_id: Global version ID
|
147
|
+
modules: List of module version information
|
148
|
+
"""
|
149
|
+
# Sort modules for consistent ordering
|
150
|
+
sorted_modules = sorted(modules, key=lambda m: (m.module_id, m.name or ""))
|
151
|
+
|
152
|
+
for i, module in enumerate(sorted_modules):
|
153
|
+
await db.execute(
|
154
|
+
"""INSERT INTO global_version_modules
|
155
|
+
(global_version_id, module_id, module_name, version,
|
156
|
+
publisher, display_name, sort_order)
|
157
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
158
|
+
(
|
159
|
+
global_version_id,
|
160
|
+
module.module_id,
|
161
|
+
module.name,
|
162
|
+
module.version,
|
163
|
+
module.publisher,
|
164
|
+
module.display_name,
|
165
|
+
i,
|
166
|
+
),
|
167
|
+
)
|
168
|
+
|
169
|
+
async def _link_environment_to_version(
|
170
|
+
self, db: aiosqlite.Connection, environment_id: int, global_version_id: int
|
171
|
+
):
|
172
|
+
"""Link environment to global version
|
173
|
+
|
174
|
+
Args:
|
175
|
+
db: Database connection
|
176
|
+
environment_id: Environment ID
|
177
|
+
global_version_id: Global version ID
|
178
|
+
"""
|
179
|
+
# Deactivate any existing active links for this environment
|
180
|
+
await db.execute(
|
181
|
+
"""UPDATE environment_versions
|
182
|
+
SET is_active = 0
|
183
|
+
WHERE environment_id = ? AND is_active = 1""",
|
184
|
+
(environment_id,),
|
185
|
+
)
|
186
|
+
|
187
|
+
# Create or reactivate link
|
188
|
+
await db.execute(
|
189
|
+
"""INSERT OR REPLACE INTO environment_versions
|
190
|
+
(environment_id, global_version_id, is_active, sync_status)
|
191
|
+
VALUES (?, ?, 1, 'pending')""",
|
192
|
+
(environment_id, global_version_id),
|
193
|
+
)
|
194
|
+
|
195
|
+
async def get_environment_version_info(
|
196
|
+
self, environment_id: int
|
197
|
+
) -> Optional[Tuple[int, EnvironmentVersionInfo]]:
|
198
|
+
"""Get current version info for environment
|
199
|
+
|
200
|
+
Args:
|
201
|
+
environment_id: Environment ID
|
202
|
+
|
203
|
+
Returns:
|
204
|
+
Tuple of (global_version_id, EnvironmentVersionInfo) if found, None otherwise
|
205
|
+
"""
|
206
|
+
async with aiosqlite.connect(self.db_path) as db:
|
207
|
+
cursor = await db.execute(
|
208
|
+
"""SELECT
|
209
|
+
ev.global_version_id,
|
210
|
+
ev.detected_at,
|
211
|
+
ev.sync_status,
|
212
|
+
gv.version_hash,
|
213
|
+
gv.modules_hash,
|
214
|
+
gv.reference_count
|
215
|
+
FROM environment_versions ev
|
216
|
+
JOIN global_versions gv ON ev.global_version_id = gv.id
|
217
|
+
WHERE ev.environment_id = ? AND ev.is_active = 1""",
|
218
|
+
(environment_id,),
|
219
|
+
)
|
220
|
+
|
221
|
+
row = await cursor.fetchone()
|
222
|
+
if not row:
|
223
|
+
return None
|
224
|
+
|
225
|
+
# Get modules for this version
|
226
|
+
modules = await self._get_global_version_modules(db, row[0])
|
227
|
+
|
228
|
+
version_info = EnvironmentVersionInfo(
|
229
|
+
environment_id=environment_id,
|
230
|
+
version_hash=row[3],
|
231
|
+
modules_hash=row[4],
|
232
|
+
modules=modules,
|
233
|
+
computed_at=(
|
234
|
+
datetime.fromisoformat(row[1])
|
235
|
+
if row[1]
|
236
|
+
else datetime.now(timezone.utc)
|
237
|
+
),
|
238
|
+
is_active=True,
|
239
|
+
)
|
240
|
+
|
241
|
+
return row[0], version_info # Return (global_version_id, version_info)
|
242
|
+
|
243
|
+
async def _get_global_version_modules(
|
244
|
+
self, db: aiosqlite.Connection, global_version_id: int
|
245
|
+
) -> List[ModuleVersionInfo]:
|
246
|
+
"""Get modules for global version
|
247
|
+
|
248
|
+
Args:
|
249
|
+
db: Database connection
|
250
|
+
global_version_id: Global version ID
|
251
|
+
|
252
|
+
Returns:
|
253
|
+
List of module version information
|
254
|
+
"""
|
255
|
+
cursor = await db.execute(
|
256
|
+
"""SELECT module_id, module_name, version, publisher, display_name
|
257
|
+
FROM global_version_modules
|
258
|
+
WHERE global_version_id = ?
|
259
|
+
ORDER BY sort_order""",
|
260
|
+
(global_version_id,),
|
261
|
+
)
|
262
|
+
|
263
|
+
modules = []
|
264
|
+
for row in await cursor.fetchall():
|
265
|
+
modules.append(
|
266
|
+
ModuleVersionInfo(
|
267
|
+
module_id=row[0],
|
268
|
+
name=row[1],
|
269
|
+
version=row[2],
|
270
|
+
publisher=row[3],
|
271
|
+
display_name=row[4],
|
272
|
+
)
|
273
|
+
)
|
274
|
+
|
275
|
+
return modules
|
276
|
+
|
277
|
+
async def get_global_version_info(
|
278
|
+
self, global_version_id: int
|
279
|
+
) -> Optional[GlobalVersionInfo]:
|
280
|
+
"""Get global version information
|
281
|
+
|
282
|
+
Args:
|
283
|
+
global_version_id: Global version ID
|
284
|
+
|
285
|
+
Returns:
|
286
|
+
Global version info if found
|
287
|
+
"""
|
288
|
+
async with aiosqlite.connect(self.db_path) as db:
|
289
|
+
cursor = await db.execute(
|
290
|
+
"""SELECT
|
291
|
+
id, version_hash, modules_hash, first_seen_at,
|
292
|
+
last_used_at, reference_count, metadata_size_bytes,
|
293
|
+
created_by_environment_id
|
294
|
+
FROM global_versions
|
295
|
+
WHERE id = ?""",
|
296
|
+
(global_version_id,),
|
297
|
+
)
|
298
|
+
|
299
|
+
row = await cursor.fetchone()
|
300
|
+
if not row:
|
301
|
+
return None
|
302
|
+
|
303
|
+
# Get modules
|
304
|
+
modules = await self._get_global_version_modules(db, global_version_id)
|
305
|
+
|
306
|
+
# Get linked environments
|
307
|
+
cursor = await db.execute(
|
308
|
+
"""SELECT me.base_url, me.environment_name
|
309
|
+
FROM environment_versions ev
|
310
|
+
JOIN metadata_environments me ON ev.environment_id = me.id
|
311
|
+
WHERE ev.global_version_id = ? AND ev.is_active = 1""",
|
312
|
+
(global_version_id,),
|
313
|
+
)
|
314
|
+
|
315
|
+
environments = []
|
316
|
+
for env_row in await cursor.fetchall():
|
317
|
+
environments.append(
|
318
|
+
{"base_url": env_row[0], "environment_name": env_row[1]}
|
319
|
+
)
|
320
|
+
|
321
|
+
return GlobalVersionInfo(
|
322
|
+
id=row[0],
|
323
|
+
version_hash=row[1],
|
324
|
+
modules_hash=row[2],
|
325
|
+
first_seen_at=datetime.fromisoformat(row[3]),
|
326
|
+
last_used_at=datetime.fromisoformat(row[4]),
|
327
|
+
reference_count=row[5],
|
328
|
+
sample_modules=(
|
329
|
+
modules[:10] if modules else []
|
330
|
+
), # Use first 10 modules as sample
|
331
|
+
)
|
332
|
+
|
333
|
+
async def find_compatible_versions(
|
334
|
+
self, modules: List[ModuleVersionInfo], exact_match: bool = False
|
335
|
+
) -> List[GlobalVersionInfo]:
|
336
|
+
"""Find compatible global versions
|
337
|
+
|
338
|
+
Args:
|
339
|
+
modules: Target modules to find compatibility for
|
340
|
+
exact_match: If True, require exact module match
|
341
|
+
|
342
|
+
Returns:
|
343
|
+
List of compatible global versions, sorted by compatibility
|
344
|
+
"""
|
345
|
+
target_modules_hash = self._calculate_modules_hash(modules)
|
346
|
+
|
347
|
+
async with aiosqlite.connect(self.db_path) as db:
|
348
|
+
if exact_match:
|
349
|
+
# Exact module match only
|
350
|
+
cursor = await db.execute(
|
351
|
+
"""SELECT id FROM global_versions
|
352
|
+
WHERE modules_hash = ?
|
353
|
+
ORDER BY last_used_at DESC""",
|
354
|
+
(target_modules_hash,),
|
355
|
+
)
|
356
|
+
else:
|
357
|
+
# Get all versions for compatibility analysis
|
358
|
+
cursor = await db.execute(
|
359
|
+
"""SELECT id FROM global_versions
|
360
|
+
ORDER BY reference_count DESC, last_used_at DESC"""
|
361
|
+
)
|
362
|
+
|
363
|
+
compatible_versions = []
|
364
|
+
for row in await cursor.fetchall():
|
365
|
+
version_info = await self.get_global_version_info(row[0])
|
366
|
+
if version_info:
|
367
|
+
if exact_match or self._is_compatible(
|
368
|
+
modules, version_info.modules
|
369
|
+
):
|
370
|
+
compatible_versions.append(version_info)
|
371
|
+
|
372
|
+
return compatible_versions
|
373
|
+
|
374
|
+
def _is_compatible(
|
375
|
+
self,
|
376
|
+
target_modules: List[ModuleVersionInfo],
|
377
|
+
candidate_modules: List[ModuleVersionInfo],
|
378
|
+
) -> bool:
|
379
|
+
"""Check if modules are compatible
|
380
|
+
|
381
|
+
Args:
|
382
|
+
target_modules: Target modules
|
383
|
+
candidate_modules: Candidate modules to check
|
384
|
+
|
385
|
+
Returns:
|
386
|
+
True if modules are compatible
|
387
|
+
"""
|
388
|
+
# Create dictionaries for easier lookup
|
389
|
+
target_dict = {m.module_id: m for m in target_modules}
|
390
|
+
candidate_dict = {m.module_id: m for m in candidate_modules}
|
391
|
+
|
392
|
+
# Check core modules compatibility
|
393
|
+
core_modules = [
|
394
|
+
"ApplicationPlatform",
|
395
|
+
"ApplicationFoundation",
|
396
|
+
"ApplicationSuite",
|
397
|
+
]
|
398
|
+
|
399
|
+
for core_module in core_modules:
|
400
|
+
target_module = target_dict.get(core_module)
|
401
|
+
candidate_module = candidate_dict.get(core_module)
|
402
|
+
|
403
|
+
if target_module and candidate_module:
|
404
|
+
if target_module.version != candidate_module.version:
|
405
|
+
return False
|
406
|
+
elif target_module or candidate_module:
|
407
|
+
# One has core module, other doesn't - incompatible
|
408
|
+
return False
|
409
|
+
|
410
|
+
# For now, require exact core module match
|
411
|
+
# Future: implement semantic version compatibility
|
412
|
+
return True
|
413
|
+
|
414
|
+
async def cleanup_unused_versions(self, max_unused_days: int = 30) -> int:
|
415
|
+
"""Clean up unused global versions
|
416
|
+
|
417
|
+
Args:
|
418
|
+
max_unused_days: Days after which unused versions can be cleaned
|
419
|
+
|
420
|
+
Returns:
|
421
|
+
Number of versions cleaned up
|
422
|
+
"""
|
423
|
+
cutoff_date = datetime.now(timezone.utc).replace(tzinfo=None)
|
424
|
+
cutoff_date = cutoff_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
425
|
+
cutoff_date = cutoff_date.timestamp() - (max_unused_days * 86400)
|
426
|
+
cutoff_timestamp = datetime.fromtimestamp(cutoff_date).isoformat()
|
427
|
+
|
428
|
+
async with aiosqlite.connect(self.db_path) as db:
|
429
|
+
# Find unused versions
|
430
|
+
cursor = await db.execute(
|
431
|
+
"""SELECT id FROM global_versions
|
432
|
+
WHERE reference_count = 0
|
433
|
+
AND last_used_at < ?""",
|
434
|
+
(cutoff_timestamp,),
|
435
|
+
)
|
436
|
+
|
437
|
+
unused_versions = [row[0] for row in await cursor.fetchall()]
|
438
|
+
|
439
|
+
if not unused_versions:
|
440
|
+
return 0
|
441
|
+
|
442
|
+
# Delete related data
|
443
|
+
for global_version_id in unused_versions:
|
444
|
+
await self._delete_global_version_data(db, global_version_id)
|
445
|
+
|
446
|
+
await db.commit()
|
447
|
+
|
448
|
+
logger.info(f"Cleaned up {len(unused_versions)} unused global versions")
|
449
|
+
return len(unused_versions)
|
450
|
+
|
451
|
+
async def _delete_global_version_data(
|
452
|
+
self, db: aiosqlite.Connection, global_version_id: int
|
453
|
+
):
|
454
|
+
"""Delete all data for a global version
|
455
|
+
|
456
|
+
Args:
|
457
|
+
db: Database connection
|
458
|
+
global_version_id: Global version ID to delete
|
459
|
+
"""
|
460
|
+
# Delete in dependency order
|
461
|
+
tables = [
|
462
|
+
"action_parameters",
|
463
|
+
"property_group_members",
|
464
|
+
"relation_constraints",
|
465
|
+
"entity_properties",
|
466
|
+
"navigation_properties",
|
467
|
+
"property_groups",
|
468
|
+
"entity_actions",
|
469
|
+
"enumeration_members",
|
470
|
+
"data_entities",
|
471
|
+
"public_entities",
|
472
|
+
"enumerations",
|
473
|
+
"labels_cache",
|
474
|
+
"global_version_modules",
|
475
|
+
"metadata_versions",
|
476
|
+
"environment_versions",
|
477
|
+
"global_versions",
|
478
|
+
]
|
479
|
+
|
480
|
+
for table in tables:
|
481
|
+
if table == "global_versions":
|
482
|
+
# Final delete of the version record
|
483
|
+
await db.execute(
|
484
|
+
f"DELETE FROM {table} WHERE id = ?", (global_version_id,)
|
485
|
+
)
|
486
|
+
else:
|
487
|
+
# Delete related records
|
488
|
+
await db.execute(
|
489
|
+
f"DELETE FROM {table} WHERE global_version_id = ?",
|
490
|
+
(global_version_id,),
|
491
|
+
)
|
492
|
+
|
493
|
+
async def update_sync_status(
|
494
|
+
self,
|
495
|
+
environment_id: int,
|
496
|
+
global_version_id: int,
|
497
|
+
status: str,
|
498
|
+
duration_ms: Optional[int] = None,
|
499
|
+
):
|
500
|
+
"""Update sync status for environment version
|
501
|
+
|
502
|
+
Args:
|
503
|
+
environment_id: Environment ID
|
504
|
+
global_version_id: Global version ID
|
505
|
+
status: New sync status
|
506
|
+
duration_ms: Sync duration in milliseconds
|
507
|
+
"""
|
508
|
+
async with aiosqlite.connect(self.db_path) as db:
|
509
|
+
if duration_ms is not None:
|
510
|
+
await db.execute(
|
511
|
+
"""UPDATE environment_versions
|
512
|
+
SET sync_status = ?, last_sync_duration_ms = ?
|
513
|
+
WHERE environment_id = ? AND global_version_id = ?""",
|
514
|
+
(status, duration_ms, environment_id, global_version_id),
|
515
|
+
)
|
516
|
+
else:
|
517
|
+
await db.execute(
|
518
|
+
"""UPDATE environment_versions
|
519
|
+
SET sync_status = ?
|
520
|
+
WHERE environment_id = ? AND global_version_id = ?""",
|
521
|
+
(status, environment_id, global_version_id),
|
522
|
+
)
|
523
|
+
|
524
|
+
await db.commit()
|
525
|
+
|
526
|
+
async def get_version_statistics(self) -> Dict[str, Any]:
|
527
|
+
"""Get global version statistics
|
528
|
+
|
529
|
+
Returns:
|
530
|
+
Dictionary with version statistics
|
531
|
+
"""
|
532
|
+
async with aiosqlite.connect(self.db_path) as db:
|
533
|
+
stats = {}
|
534
|
+
|
535
|
+
# Basic counts
|
536
|
+
cursor = await db.execute("SELECT COUNT(*) FROM global_versions")
|
537
|
+
stats["total_versions"] = (await cursor.fetchone())[0]
|
538
|
+
|
539
|
+
cursor = await db.execute(
|
540
|
+
"SELECT COUNT(DISTINCT environment_id) FROM environment_versions"
|
541
|
+
)
|
542
|
+
stats["total_environments"] = (await cursor.fetchone())[0]
|
543
|
+
|
544
|
+
# Reference statistics
|
545
|
+
cursor = await db.execute(
|
546
|
+
"""SELECT
|
547
|
+
SUM(reference_count) as total_references,
|
548
|
+
AVG(reference_count) as avg_references,
|
549
|
+
MAX(reference_count) as max_references,
|
550
|
+
COUNT(*) as versions_with_refs
|
551
|
+
FROM global_versions
|
552
|
+
WHERE reference_count > 0"""
|
553
|
+
)
|
554
|
+
ref_stats = await cursor.fetchone()
|
555
|
+
stats["reference_statistics"] = {
|
556
|
+
"total_references": ref_stats[0] or 0,
|
557
|
+
"avg_references": round(ref_stats[1] or 0, 2),
|
558
|
+
"max_references": ref_stats[2] or 0,
|
559
|
+
"versions_with_references": ref_stats[3] or 0,
|
560
|
+
}
|
561
|
+
|
562
|
+
# Version age statistics
|
563
|
+
cursor = await db.execute(
|
564
|
+
"""SELECT
|
565
|
+
COUNT(*) as recent_versions
|
566
|
+
FROM global_versions
|
567
|
+
WHERE last_used_at >= datetime('now', '-7 days')"""
|
568
|
+
)
|
569
|
+
stats["recent_activity"] = {
|
570
|
+
"versions_used_last_7_days": (await cursor.fetchone())[0]
|
571
|
+
}
|
572
|
+
|
573
|
+
return stats
|