ucapi-framework 1.9.1__tar.gz → 1.9.3__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.
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/PKG-INFO +1 -1
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/pyproject.toml +1 -1
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_discovery.py +67 -8
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/__init__.py +2 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/discovery.py +24 -21
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/helpers.py +163 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework.egg-info/PKG-INFO +1 -1
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/README.md +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/setup.cfg +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_button_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_climate_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_config.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_cover_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_device.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_driver.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_factory_pattern.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_helpers.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_ir_emitter_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_light_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_media_player_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_migration.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_remote_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_select_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_sensor_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_setup.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_switch_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/tests/test_voice_assistant_entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/config.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/device.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/driver.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/__init__.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/button.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/climate.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/cover.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/ir_emitter.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/light.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/media_player.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/remote.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/select.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/sensor.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/switch.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entities/voice_assistant.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/entity.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/migration.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework/setup.py +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework.egg-info/SOURCES.txt +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework.egg-info/dependency_links.txt +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework.egg-info/requires.txt +0 -0
- {ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework.egg-info/top_level.txt +0 -0
|
@@ -451,28 +451,87 @@ class TestMDNSDiscovery:
|
|
|
451
451
|
|
|
452
452
|
@pytest.mark.asyncio
|
|
453
453
|
async def test_discover_with_zeroconf(self):
|
|
454
|
-
"""Test mDNS discovery with mocked zeroconf."""
|
|
454
|
+
"""Test mDNS discovery with mocked zeroconf async classes."""
|
|
455
455
|
mock_service_info = Mock()
|
|
456
456
|
mock_service_info.name = "test-device._test._tcp.local."
|
|
457
457
|
mock_service_info.server = "Test Device"
|
|
458
458
|
mock_service_info.addresses = [b"\xc0\xa8\x01\x64"] # 192.168.1.100 as bytes
|
|
459
459
|
mock_service_info.port = 8080
|
|
460
460
|
|
|
461
|
-
|
|
462
|
-
|
|
461
|
+
# ServiceListener must be a real class so Listener can inherit from it
|
|
462
|
+
class MockServiceListener:
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
# Track the Listener instance created inside discover()
|
|
466
|
+
captured_listener: dict = {}
|
|
467
|
+
|
|
468
|
+
# Mock AsyncZeroconf
|
|
469
|
+
mock_aiozc = AsyncMock()
|
|
470
|
+
mock_aiozc.zeroconf = Mock()
|
|
471
|
+
mock_aiozc.async_get_service_info = AsyncMock(return_value=mock_service_info)
|
|
472
|
+
mock_aiozc.async_close = AsyncMock()
|
|
473
|
+
|
|
474
|
+
# Mock AsyncServiceBrowser – capture the listener so we can call add_service
|
|
475
|
+
def mock_browser_init(zc, service_type, listener):
|
|
476
|
+
captured_listener["obj"] = listener
|
|
477
|
+
browser = Mock()
|
|
478
|
+
browser.async_cancel = AsyncMock()
|
|
479
|
+
return browser
|
|
463
480
|
|
|
464
481
|
mock_zeroconf_module = Mock()
|
|
465
|
-
mock_zeroconf_module.
|
|
466
|
-
|
|
482
|
+
mock_zeroconf_module.ServiceListener = MockServiceListener
|
|
483
|
+
|
|
484
|
+
mock_asyncio_module = Mock()
|
|
485
|
+
mock_asyncio_module.AsyncZeroconf = Mock(return_value=mock_aiozc)
|
|
486
|
+
mock_asyncio_module.AsyncServiceBrowser = Mock(side_effect=mock_browser_init)
|
|
487
|
+
|
|
488
|
+
with (
|
|
489
|
+
patch.dict(
|
|
490
|
+
"sys.modules",
|
|
491
|
+
{
|
|
492
|
+
"zeroconf": mock_zeroconf_module,
|
|
493
|
+
"zeroconf.asyncio": mock_asyncio_module,
|
|
494
|
+
},
|
|
495
|
+
),
|
|
496
|
+
patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
|
497
|
+
):
|
|
498
|
+
|
|
499
|
+
async def trigger_discovery(*args, **kwargs):
|
|
500
|
+
listener = captured_listener.get("obj")
|
|
501
|
+
if listener:
|
|
502
|
+
listener.add_service(
|
|
503
|
+
mock_aiozc.zeroconf,
|
|
504
|
+
"_test._tcp.local.",
|
|
505
|
+
"test-device._test._tcp.local.",
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
mock_sleep.side_effect = trigger_discovery
|
|
467
509
|
|
|
468
|
-
with patch.dict("sys.modules", {"zeroconf": mock_zeroconf_module}):
|
|
469
510
|
discovery = ConcreteMDNSDiscovery(
|
|
470
511
|
service_type="_test._tcp.local.", timeout=1
|
|
471
512
|
)
|
|
472
513
|
devices = await discovery.discover()
|
|
473
514
|
|
|
474
|
-
|
|
475
|
-
assert
|
|
515
|
+
assert len(devices) == 1
|
|
516
|
+
assert devices[0].identifier == "test-device._test._tcp.local."
|
|
517
|
+
assert devices[0].name == "Test Device"
|
|
518
|
+
mock_aiozc.async_get_service_info.assert_called_once_with(
|
|
519
|
+
"_test._tcp.local.", "test-device._test._tcp.local."
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
@pytest.mark.asyncio
|
|
523
|
+
async def test_discover_handles_import_error(self):
|
|
524
|
+
"""Test that discover handles missing zeroconf gracefully."""
|
|
525
|
+
discovery = ConcreteMDNSDiscovery(service_type="_test._tcp.local.", timeout=1)
|
|
526
|
+
|
|
527
|
+
def mock_import(name, *args, **kwargs):
|
|
528
|
+
if name in ("zeroconf", "zeroconf.asyncio"):
|
|
529
|
+
raise ImportError(f"No module named '{name}'")
|
|
530
|
+
return __import__(name, *args, **kwargs)
|
|
531
|
+
|
|
532
|
+
with patch("builtins.__import__", side_effect=mock_import):
|
|
533
|
+
devices = await discovery.discover()
|
|
534
|
+
assert devices == []
|
|
476
535
|
|
|
477
536
|
|
|
478
537
|
class TestNetworkScanDiscovery:
|
|
@@ -48,6 +48,7 @@ from .migration import (
|
|
|
48
48
|
)
|
|
49
49
|
from .helpers import (
|
|
50
50
|
find_orphaned_entities,
|
|
51
|
+
find_unused_activity_entities,
|
|
51
52
|
EntityAttributes,
|
|
52
53
|
ButtonAttributes,
|
|
53
54
|
ClimateAttributes,
|
|
@@ -126,6 +127,7 @@ __all__ = [
|
|
|
126
127
|
"SensorEntity",
|
|
127
128
|
"SwitchEntity",
|
|
128
129
|
"VoiceAssistantEntity",
|
|
130
|
+
"find_unused_activity_entities",
|
|
129
131
|
]
|
|
130
132
|
|
|
131
133
|
__version__ = "1.9.1"
|
|
@@ -34,6 +34,7 @@ for integrations that don't use them. Install only what you need:
|
|
|
34
34
|
:license: Mozilla Public License Version 2.0, see LICENSE for more details.
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
+
import asyncio
|
|
37
38
|
import logging
|
|
38
39
|
from abc import ABC, abstractmethod
|
|
39
40
|
from dataclasses import dataclass
|
|
@@ -356,40 +357,44 @@ class MDNSDiscovery(BaseDiscovery):
|
|
|
356
357
|
|
|
357
358
|
try:
|
|
358
359
|
try:
|
|
359
|
-
from zeroconf import
|
|
360
|
+
from zeroconf import ServiceListener # type: ignore[import-not-found]
|
|
361
|
+
from zeroconf.asyncio import AsyncServiceBrowser, AsyncZeroconf
|
|
360
362
|
except ImportError as err:
|
|
361
363
|
raise ImportError(
|
|
362
364
|
"zeroconf package is required for mDNS discovery. "
|
|
363
365
|
"Install it with: pip install zeroconf"
|
|
364
366
|
) from err
|
|
365
367
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
zeroconf = Zeroconf()
|
|
369
|
-
discovered = []
|
|
368
|
+
discovered_names: list[tuple[str, str]] = []
|
|
370
369
|
|
|
371
370
|
class Listener(ServiceListener):
|
|
372
|
-
|
|
373
|
-
info = zc.get_service_info(type_, name)
|
|
374
|
-
if info:
|
|
375
|
-
discovered.append(info)
|
|
371
|
+
"""Listener for mDNS service discovery events."""
|
|
376
372
|
|
|
377
|
-
def
|
|
378
|
-
|
|
373
|
+
def add_service(self, zc, type_: str, name: str) -> None:
|
|
374
|
+
discovered_names.append((type_, name))
|
|
379
375
|
|
|
380
|
-
def
|
|
376
|
+
def remove_service(self, zc, type_: str, name: str) -> None:
|
|
381
377
|
pass
|
|
382
378
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
# Wait for discovery timeout
|
|
386
|
-
await asyncio.sleep(self.timeout)
|
|
379
|
+
def update_service(self, zc, type_: str, name: str) -> None:
|
|
380
|
+
pass
|
|
387
381
|
|
|
388
|
-
|
|
389
|
-
|
|
382
|
+
aiozc = AsyncZeroconf()
|
|
383
|
+
try:
|
|
384
|
+
browser = AsyncServiceBrowser(
|
|
385
|
+
aiozc.zeroconf, self.service_type, Listener()
|
|
386
|
+
)
|
|
387
|
+
await asyncio.sleep(self.timeout)
|
|
388
|
+
await browser.async_cancel()
|
|
389
|
+
discovered = [
|
|
390
|
+
info
|
|
391
|
+
for type_, name in discovered_names
|
|
392
|
+
if (info := await aiozc.async_get_service_info(type_, name))
|
|
393
|
+
]
|
|
394
|
+
finally:
|
|
395
|
+
await aiozc.async_close()
|
|
390
396
|
|
|
391
397
|
self._discovered_devices.clear()
|
|
392
|
-
|
|
393
398
|
for service_info in discovered:
|
|
394
399
|
device = self.parse_mdns_service(service_info)
|
|
395
400
|
if device:
|
|
@@ -399,10 +404,8 @@ class MDNSDiscovery(BaseDiscovery):
|
|
|
399
404
|
"mDNS discovery complete: found %d device(s)",
|
|
400
405
|
len(self._discovered_devices),
|
|
401
406
|
)
|
|
402
|
-
|
|
403
407
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
404
408
|
_LOG.error("mDNS discovery error: %s", err)
|
|
405
|
-
|
|
406
409
|
return self._discovered_devices
|
|
407
410
|
|
|
408
411
|
@abstractmethod
|
|
@@ -294,3 +294,166 @@ async def find_orphaned_entities(
|
|
|
294
294
|
except Exception as err: # pylint: disable=broad-except
|
|
295
295
|
_LOG.error("Unexpected error while scanning for orphaned entities: %s", err)
|
|
296
296
|
return orphaned_entities
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _extract_used_entity_ids(activity: dict[str, Any]) -> set[str]:
|
|
300
|
+
"""
|
|
301
|
+
Extract all entity IDs that are actively used in an activity's sequences,
|
|
302
|
+
button_mapping, and user_interface.
|
|
303
|
+
|
|
304
|
+
:param activity: Full activity dict from GET /api/activities/{id}
|
|
305
|
+
:return: Set of entity_id strings that are referenced
|
|
306
|
+
"""
|
|
307
|
+
used: set[str] = set()
|
|
308
|
+
options = activity.get("options", {})
|
|
309
|
+
|
|
310
|
+
# sequences: on/off lists of steps; each step may have command.entity_id
|
|
311
|
+
sequences = options.get("sequences", {})
|
|
312
|
+
for steps in sequences.values():
|
|
313
|
+
if not isinstance(steps, list):
|
|
314
|
+
continue
|
|
315
|
+
for step in steps:
|
|
316
|
+
cmd = step.get("command", {})
|
|
317
|
+
if isinstance(cmd, dict) and cmd.get("entity_id"):
|
|
318
|
+
used.add(cmd["entity_id"])
|
|
319
|
+
|
|
320
|
+
# button_mapping: list of {short_press, long_press} each with entity_id
|
|
321
|
+
for mapping in options.get("button_mapping", []):
|
|
322
|
+
for press_key in ("short_press", "long_press"):
|
|
323
|
+
press = mapping.get(press_key, {})
|
|
324
|
+
if isinstance(press, dict) and press.get("entity_id"):
|
|
325
|
+
used.add(press["entity_id"])
|
|
326
|
+
|
|
327
|
+
# user_interface: pages → items; entity IDs hide in several fields
|
|
328
|
+
ui = options.get("user_interface", {})
|
|
329
|
+
for page in ui.get("pages", []):
|
|
330
|
+
for item in page.get("items", []):
|
|
331
|
+
# Direct command entity
|
|
332
|
+
cmd = item.get("command", {})
|
|
333
|
+
if isinstance(cmd, dict) and cmd.get("entity_id"):
|
|
334
|
+
used.add(cmd["entity_id"])
|
|
335
|
+
# media_player_id
|
|
336
|
+
if item.get("media_player_id"):
|
|
337
|
+
used.add(item["media_player_id"])
|
|
338
|
+
# sensor_id
|
|
339
|
+
sensor = item.get("sensor", {})
|
|
340
|
+
if isinstance(sensor, dict) and sensor.get("sensor_id"):
|
|
341
|
+
used.add(sensor["sensor_id"])
|
|
342
|
+
# select_id
|
|
343
|
+
select = item.get("select", {})
|
|
344
|
+
if isinstance(select, dict) and select.get("select_id"):
|
|
345
|
+
used.add(select["select_id"])
|
|
346
|
+
|
|
347
|
+
return used
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
async def find_unused_activity_entities(
|
|
351
|
+
remote_url: str,
|
|
352
|
+
pin: str | None = None,
|
|
353
|
+
api_key: str | None = None,
|
|
354
|
+
) -> list[dict[str, Any]]:
|
|
355
|
+
"""
|
|
356
|
+
Find entities included in activities that are never actually used.
|
|
357
|
+
|
|
358
|
+
An entity is "unused" when it appears in ``included_entities`` but has no
|
|
359
|
+
reference in sequences, button_mapping, or user_interface (commands, media
|
|
360
|
+
player widgets, sensor widgets, or select widgets).
|
|
361
|
+
|
|
362
|
+
Authentication can be done via PIN (Basic Auth) or API key (Bearer token).
|
|
363
|
+
One of ``pin`` or ``api_key`` must be provided.
|
|
364
|
+
|
|
365
|
+
:param remote_url: The Remote's base URL (e.g., "http://192.168.1.100")
|
|
366
|
+
:param pin: Remote's web-configurator PIN for Basic Auth
|
|
367
|
+
:param api_key: Remote's API key for Bearer token authentication
|
|
368
|
+
:return: List of dicts with activity context and the unused entity info
|
|
369
|
+
:raises ValueError: If neither pin nor api_key is provided
|
|
370
|
+
"""
|
|
371
|
+
if not pin and not api_key:
|
|
372
|
+
raise ValueError("Either pin or api_key must be provided for authentication")
|
|
373
|
+
|
|
374
|
+
_LOG.info("Scanning for unused activity entities on Remote at %s", remote_url)
|
|
375
|
+
|
|
376
|
+
headers = {"Content-Type": "application/json"}
|
|
377
|
+
if api_key:
|
|
378
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
379
|
+
|
|
380
|
+
auth = None
|
|
381
|
+
if pin and not api_key:
|
|
382
|
+
auth = aiohttp.BasicAuth(login="web-configurator", password=pin)
|
|
383
|
+
|
|
384
|
+
unused: list[dict[str, Any]] = []
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
async with aiohttp.ClientSession() as session:
|
|
388
|
+
activities_url = f"{remote_url}/api/activities?limit=100"
|
|
389
|
+
async with session.get(
|
|
390
|
+
activities_url,
|
|
391
|
+
headers=headers,
|
|
392
|
+
auth=auth,
|
|
393
|
+
timeout=aiohttp.ClientTimeout(total=30),
|
|
394
|
+
) as resp:
|
|
395
|
+
if resp.status != 200:
|
|
396
|
+
_LOG.error("Failed to fetch activities: HTTP %d", resp.status)
|
|
397
|
+
return unused
|
|
398
|
+
activities_list = await resp.json()
|
|
399
|
+
_LOG.info(
|
|
400
|
+
"Found %d activities to scan for unused entities",
|
|
401
|
+
len(activities_list),
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
for activity_summary in activities_list:
|
|
405
|
+
activity_id = activity_summary.get("entity_id")
|
|
406
|
+
if not activity_id:
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
activity_url = f"{remote_url}/api/activities/{activity_id}"
|
|
410
|
+
async with session.get(
|
|
411
|
+
activity_url,
|
|
412
|
+
headers=headers,
|
|
413
|
+
auth=auth,
|
|
414
|
+
timeout=aiohttp.ClientTimeout(total=30),
|
|
415
|
+
) as resp:
|
|
416
|
+
if resp.status != 200:
|
|
417
|
+
_LOG.warning(
|
|
418
|
+
"Failed to fetch activity %s: HTTP %d",
|
|
419
|
+
activity_id,
|
|
420
|
+
resp.status,
|
|
421
|
+
)
|
|
422
|
+
continue
|
|
423
|
+
activity = await resp.json()
|
|
424
|
+
|
|
425
|
+
activity_name = activity_summary.get("name") or activity.get("name", {})
|
|
426
|
+
options = activity.get("options", {})
|
|
427
|
+
included_entities = options.get("included_entities", [])
|
|
428
|
+
|
|
429
|
+
if not included_entities:
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
included_ids = {
|
|
433
|
+
e["entity_id"] for e in included_entities if e.get("entity_id")
|
|
434
|
+
}
|
|
435
|
+
used_ids = _extract_used_entity_ids(activity)
|
|
436
|
+
truly_unused = included_ids - used_ids
|
|
437
|
+
|
|
438
|
+
for entity in included_entities:
|
|
439
|
+
eid = entity.get("entity_id")
|
|
440
|
+
if eid in truly_unused:
|
|
441
|
+
record = {
|
|
442
|
+
k: v
|
|
443
|
+
for k, v in entity.items()
|
|
444
|
+
if k not in ("entity_commands", "simple_commands")
|
|
445
|
+
}
|
|
446
|
+
record["activity_id"] = activity_id
|
|
447
|
+
record["activity_name"] = activity_name
|
|
448
|
+
unused.append(record)
|
|
449
|
+
_LOG.debug("Unused entity %s in activity %s", eid, activity_id)
|
|
450
|
+
|
|
451
|
+
_LOG.info("Found %d unused activity entities", len(unused))
|
|
452
|
+
return unused
|
|
453
|
+
|
|
454
|
+
except aiohttp.ClientError as err:
|
|
455
|
+
_LOG.error("Network error while scanning for unused entities: %s", err)
|
|
456
|
+
return unused
|
|
457
|
+
except Exception as err: # pylint: disable=broad-except
|
|
458
|
+
_LOG.error("Unexpected error while scanning for unused entities: %s", err)
|
|
459
|
+
return unused
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ucapi_framework-1.9.1 → ucapi_framework-1.9.3}/ucapi_framework.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|