drb 2.3.4__tar.gz → 2.3.8__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 (110) hide show
  1. {drb-2.3.4/drb.egg-info → drb-2.3.8}/PKG-INFO +1 -17
  2. {drb-2.3.4 → drb-2.3.8}/README.md +0 -16
  3. {drb-2.3.4 → drb-2.3.8}/drb/core/_version.py +3 -3
  4. drb-2.3.8/drb/core/context.py +24 -0
  5. {drb-2.3.4 → drb-2.3.8}/drb/core/node.py +7 -0
  6. drb-2.3.8/drb/core/protocol.py +58 -0
  7. {drb-2.3.4 → drb-2.3.8}/drb/signatures/core.py +34 -0
  8. drb-2.3.8/drb/signatures/hierarchy.py +124 -0
  9. {drb-2.3.4 → drb-2.3.8}/drb/topics/dao/manager_dao.py +77 -1
  10. {drb-2.3.4 → drb-2.3.8}/drb/topics/dao/rdf_dao.py +30 -3
  11. {drb-2.3.4 → drb-2.3.8}/drb/topics/resolver.py +62 -15
  12. {drb-2.3.4 → drb-2.3.8/drb.egg-info}/PKG-INFO +1 -17
  13. {drb-2.3.4 → drb-2.3.8}/drb.egg-info/SOURCES.txt +12 -0
  14. {drb-2.3.4 → drb-2.3.8}/drb.egg-info/entry_points.txt +6 -0
  15. {drb-2.3.4 → drb-2.3.8}/setup.cfg +7 -0
  16. drb-2.3.8/tests/test_implementation_coupling.py +115 -0
  17. drb-2.3.8/tests/test_manager_get_dao.py +27 -0
  18. drb-2.3.8/tests/test_node_context.py +42 -0
  19. drb-2.3.8/tests/test_protocol_dispatch.py +115 -0
  20. drb-2.3.8/tests/test_protocol_loader.py +29 -0
  21. drb-2.3.8/tests/test_rdf_dao_signatures.py +122 -0
  22. drb-2.3.8/tests/test_signature_exact.py +20 -0
  23. drb-2.3.8/tests/test_signature_hierarchy.py +105 -0
  24. drb-2.3.8/tests/test_topic_descriptor_discovery.py +55 -0
  25. {drb-2.3.4 → drb-2.3.8}/LICENCE.txt +0 -0
  26. {drb-2.3.4 → drb-2.3.8}/MANIFEST.in +0 -0
  27. {drb-2.3.4 → drb-2.3.8}/drb/addons/addon.py +0 -0
  28. {drb-2.3.4 → drb-2.3.8}/drb/core/__init__.py +0 -0
  29. {drb-2.3.4 → drb-2.3.8}/drb/core/cortex_validator.py +0 -0
  30. {drb-2.3.4 → drb-2.3.8}/drb/core/events.py +0 -0
  31. {drb-2.3.4 → drb-2.3.8}/drb/core/factory.py +0 -0
  32. {drb-2.3.4 → drb-2.3.8}/drb/core/impls.py +0 -0
  33. {drb-2.3.4 → drb-2.3.8}/drb/core/item.py +0 -0
  34. {drb-2.3.4 → drb-2.3.8}/drb/core/path.py +0 -0
  35. {drb-2.3.4 → drb-2.3.8}/drb/core/predicate.py +0 -0
  36. {drb-2.3.4 → drb-2.3.8}/drb/core/signature.py +0 -0
  37. {drb-2.3.4 → drb-2.3.8}/drb/drivers/file/__init__.py +0 -0
  38. {drb-2.3.4 → drb-2.3.8}/drb/drivers/file/file.py +0 -0
  39. {drb-2.3.4 → drb-2.3.8}/drb/drivers/xml/__init__.py +0 -0
  40. {drb-2.3.4 → drb-2.3.8}/drb/drivers/xml/factory.py +0 -0
  41. {drb-2.3.4 → drb-2.3.8}/drb/drivers/xml/node.py +0 -0
  42. {drb-2.3.4 → drb-2.3.8}/drb/exceptions/core.py +0 -0
  43. {drb-2.3.4 → drb-2.3.8}/drb/exceptions/file.py +0 -0
  44. {drb-2.3.4 → drb-2.3.8}/drb/keyring/bitwarden/__init__.py +0 -0
  45. {drb-2.3.4 → drb-2.3.8}/drb/keyring/bitwarden/bitwarden.py +0 -0
  46. {drb-2.3.4 → drb-2.3.8}/drb/nodes/abstract_node.py +0 -0
  47. {drb-2.3.4 → drb-2.3.8}/drb/nodes/logical_node.py +0 -0
  48. {drb-2.3.4 → drb-2.3.8}/drb/nodes/mutable_node.py +0 -0
  49. {drb-2.3.4 → drb-2.3.8}/drb/nodes/url_node.py +0 -0
  50. {drb-2.3.4 → drb-2.3.8}/drb/signatures/xquery.py +0 -0
  51. {drb-2.3.4 → drb-2.3.8}/drb/topics/dao/__init__.py +0 -0
  52. {drb-2.3.4 → drb-2.3.8}/drb/topics/dao/topic_dao.py +0 -0
  53. {drb-2.3.4 → drb-2.3.8}/drb/topics/dao/topic_schema.yml +0 -0
  54. {drb-2.3.4 → drb-2.3.8}/drb/topics/dao/xml_dao.py +0 -0
  55. {drb-2.3.4 → drb-2.3.8}/drb/topics/dao/yaml_dao.py +0 -0
  56. {drb-2.3.4 → drb-2.3.8}/drb/topics/file/__init__.py +0 -0
  57. {drb-2.3.4 → drb-2.3.8}/drb/topics/file/cortex.yml +0 -0
  58. {drb-2.3.4 → drb-2.3.8}/drb/topics/topic.py +0 -0
  59. {drb-2.3.4 → drb-2.3.8}/drb/topics/xml/__init__.py +0 -0
  60. {drb-2.3.4 → drb-2.3.8}/drb/topics/xml/cortex.yml +0 -0
  61. {drb-2.3.4 → drb-2.3.8}/drb/utils/__init__.py +0 -0
  62. {drb-2.3.4 → drb-2.3.8}/drb/utils/drb_python_script.py +0 -0
  63. {drb-2.3.4 → drb-2.3.8}/drb/utils/plugins.py +0 -0
  64. {drb-2.3.4 → drb-2.3.8}/drb/utils/url_security.py +0 -0
  65. {drb-2.3.4 → drb-2.3.8}/drb/utils/utils.py +0 -0
  66. {drb-2.3.4 → drb-2.3.8}/drb/xquery/XQueryLexer.py +0 -0
  67. {drb-2.3.4 → drb-2.3.8}/drb/xquery/XQueryParser.py +0 -0
  68. {drb-2.3.4 → drb-2.3.8}/drb/xquery/XQueryParserListener.py +0 -0
  69. {drb-2.3.4 → drb-2.3.8}/drb/xquery/XQueryParserVisitor.py +0 -0
  70. {drb-2.3.4 → drb-2.3.8}/drb/xquery/__init__.py +0 -0
  71. {drb-2.3.4 → drb-2.3.8}/drb/xquery/drb_xquery.py +0 -0
  72. {drb-2.3.4 → drb-2.3.8}/drb/xquery/drb_xquery_cmd.py +0 -0
  73. {drb-2.3.4 → drb-2.3.8}/drb/xquery/drb_xquery_context.py +0 -0
  74. {drb-2.3.4 → drb-2.3.8}/drb/xquery/drb_xquery_func.py +0 -0
  75. {drb-2.3.4 → drb-2.3.8}/drb/xquery/drb_xquery_item.py +0 -0
  76. {drb-2.3.4 → drb-2.3.8}/drb/xquery/drb_xquery_res_to_string.py +0 -0
  77. {drb-2.3.4 → drb-2.3.8}/drb/xquery/drb_xquery_utils.py +0 -0
  78. {drb-2.3.4 → drb-2.3.8}/drb/xquery/drb_xquery_variable.py +0 -0
  79. {drb-2.3.4 → drb-2.3.8}/drb/xquery/drb_xquery_visitor.py +0 -0
  80. {drb-2.3.4 → drb-2.3.8}/drb/xquery/exceptions.py +0 -0
  81. {drb-2.3.4 → drb-2.3.8}/drb.egg-info/dependency_links.txt +0 -0
  82. {drb-2.3.4 → drb-2.3.8}/drb.egg-info/not-zip-safe +0 -0
  83. {drb-2.3.4 → drb-2.3.8}/drb.egg-info/requires.txt +0 -0
  84. {drb-2.3.4 → drb-2.3.8}/drb.egg-info/top_level.txt +0 -0
  85. {drb-2.3.4 → drb-2.3.8}/pyproject.toml +0 -0
  86. {drb-2.3.4 → drb-2.3.8}/requirements.txt +0 -0
  87. {drb-2.3.4 → drb-2.3.8}/setup.py +0 -0
  88. {drb-2.3.4 → drb-2.3.8}/tests/test_abstract_node.py +0 -0
  89. {drb-2.3.4 → drb-2.3.8}/tests/test_addon.py +0 -0
  90. {drb-2.3.4 → drb-2.3.8}/tests/test_children_unresolved.py +0 -0
  91. {drb-2.3.4 → drb-2.3.8}/tests/test_drb_node_list.py +0 -0
  92. {drb-2.3.4 → drb-2.3.8}/tests/test_drb_python_script.py +0 -0
  93. {drb-2.3.4 → drb-2.3.8}/tests/test_event.py +0 -0
  94. {drb-2.3.4 → drb-2.3.8}/tests/test_factory.py +0 -0
  95. {drb-2.3.4 → drb-2.3.8}/tests/test_factory_resolver.py +0 -0
  96. {drb-2.3.4 → drb-2.3.8}/tests/test_implementation_manager.py +0 -0
  97. {drb-2.3.4 → drb-2.3.8}/tests/test_keyring_bitwarden.py +0 -0
  98. {drb-2.3.4 → drb-2.3.8}/tests/test_logical_node.py +0 -0
  99. {drb-2.3.4 → drb-2.3.8}/tests/test_manager_dao.py +0 -0
  100. {drb-2.3.4 → drb-2.3.8}/tests/test_node_descendant.py +0 -0
  101. {drb-2.3.4 → drb-2.3.8}/tests/test_path.py +0 -0
  102. {drb-2.3.4 → drb-2.3.8}/tests/test_rdf_dao.py +0 -0
  103. {drb-2.3.4 → drb-2.3.8}/tests/test_rdf_dao_multi_url.py +0 -0
  104. {drb-2.3.4 → drb-2.3.8}/tests/test_signatures.py +0 -0
  105. {drb-2.3.4 → drb-2.3.8}/tests/test_topic.py +0 -0
  106. {drb-2.3.4 → drb-2.3.8}/tests/test_url_node.py +0 -0
  107. {drb-2.3.4 → drb-2.3.8}/tests/test_url_security.py +0 -0
  108. {drb-2.3.4 → drb-2.3.8}/tests/test_xml_dao.py +0 -0
  109. {drb-2.3.4 → drb-2.3.8}/tests/test_yaml_dao.py +0 -0
  110. {drb-2.3.4 → drb-2.3.8}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drb
3
- Version: 2.3.4
3
+ Version: 2.3.8
4
4
  Summary: Data Request Broker
5
5
  Author: GAEL Systems
6
6
  Author-email: drb-python@gael.fr
@@ -54,22 +54,6 @@ Once successfully run, the command line prompt is updated with the virtual envir
54
54
  Once the virtual environment installed, the dependencies shall be downloaded:
55
55
  ```commandline
56
56
  (venv) $~> pip install -r requirements.txt --no-cache-dir
57
- Collecting pip==21.1.2
58
- Downloading pip-21.1.2-py3-none-any.whl (1.5 MB)
59
- |████████████████████████████████| 1.5 MB 54 kB/s
60
- Collecting setuptools==57.0.0
61
- Downloading setuptools-57.0.0-py3-none-any.whl (821 kB)
62
- |████████████████████████████████| 821 kB 73 kB/s
63
- Installing collected packages: pip, setuptools
64
- Attempting uninstall: pip
65
- Found existing installation: pip 20.0.2
66
- Uninstalling pip-20.0.2:
67
- Successfully uninstalled pip-20.0.2
68
- Attempting uninstall: setuptools
69
- Found existing installation: setuptools 44.0.0
70
- Uninstalling setuptools-44.0.0:
71
- Successfully uninstalled setuptools-44.0.0
72
- Successfully installed pip-21.1.2 setuptools-57.0.0
73
57
  ```
74
58
  At this stage the environment is installed.
75
59
 
@@ -25,22 +25,6 @@ Once successfully run, the command line prompt is updated with the virtual envir
25
25
  Once the virtual environment installed, the dependencies shall be downloaded:
26
26
  ```commandline
27
27
  (venv) $~> pip install -r requirements.txt --no-cache-dir
28
- Collecting pip==21.1.2
29
- Downloading pip-21.1.2-py3-none-any.whl (1.5 MB)
30
- |████████████████████████████████| 1.5 MB 54 kB/s
31
- Collecting setuptools==57.0.0
32
- Downloading setuptools-57.0.0-py3-none-any.whl (821 kB)
33
- |████████████████████████████████| 821 kB 73 kB/s
34
- Installing collected packages: pip, setuptools
35
- Attempting uninstall: pip
36
- Found existing installation: pip 20.0.2
37
- Uninstalling pip-20.0.2:
38
- Successfully uninstalled pip-20.0.2
39
- Attempting uninstall: setuptools
40
- Found existing installation: setuptools 44.0.0
41
- Uninstalling setuptools-44.0.0:
42
- Successfully uninstalled setuptools-44.0.0
43
- Successfully installed pip-21.1.2 setuptools-57.0.0
44
28
  ```
45
29
  At this stage the environment is installed.
46
30
 
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2026-06-19T11:01:35+0000",
11
+ "date": "2026-06-25T20:48:59+0000",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "ad5712117cb18c0a99fbddcf98e59256eafd435e",
15
- "version": "2.3.4"
14
+ "full-revisionid": "15fc9c3ff4c5c9f27c0ec6e7bba7d7c3e3885305",
15
+ "version": "2.3.8"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -0,0 +1,24 @@
1
+ from typing import Dict
2
+
3
+
4
+ class Context:
5
+ """Per-node store of named context buckets.
6
+
7
+ A bucket is a plain ``dict`` keyed by a name (e.g. ``'resolver'`` for
8
+ resolution state, ``'metadata'`` for an add-on's extracted values).
9
+ Buckets are created lazily on first access, so any consumer
10
+ (resolver, add-on) can stash per-node state without the node knowing
11
+ about it:
12
+
13
+ node.context['resolver']['topic'] = topic
14
+ node.context['metadata']['platform'] = 'S2A'
15
+ """
16
+
17
+ def __init__(self):
18
+ self._buckets: Dict[str, dict] = {}
19
+
20
+ def __getitem__(self, name: str) -> dict:
21
+ return self._buckets.setdefault(name, {})
22
+
23
+ def __contains__(self, name: str) -> bool:
24
+ return name in self._buckets
@@ -9,6 +9,7 @@ from deprecated import deprecated
9
9
 
10
10
  from .item import DrbItem
11
11
  from .path import ParsedPath, parse_path
12
+ from .context import Context
12
13
  from .impls import ImplementationManager
13
14
  from ..exceptions.core import DrbException, DrbNotImplementationException
14
15
  from ..utils.utils import get_name_namespace_index, get_name_namespace_value
@@ -42,6 +43,7 @@ class DrbNode(DrbItem, abc.ABC):
42
43
  self.__namespace_aware = None
43
44
  self._attrs = {}
44
45
  self._parent = None
46
+ self._context = Context()
45
47
  self._impl_mng = ImplementationManager()
46
48
  for event_name in self.__event_type_node:
47
49
  self._event_manager.append_event_type(event_name)
@@ -464,5 +466,10 @@ class DrbNode(DrbItem, abc.ABC):
464
466
  return self.has_child(*item)
465
467
  return False
466
468
 
469
+ @property
470
+ def context(self) -> Context:
471
+ """Per-node store of named context buckets (resolver, add-ons...)."""
472
+ return self._context
473
+
467
474
  def __eq__(self, other):
468
475
  return self.path == other.path
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import inspect
5
+ import logging
6
+ from typing import Optional
7
+
8
+ import drb.utils.plugins
9
+ from drb.core.factory import DrbFactory
10
+
11
+
12
+ class ProtocolLoader:
13
+ """Loads ``drb.protocol`` entry points into a scheme -> factory map.
14
+
15
+ Each entry point's name is a URL scheme (e.g. ``s3``, ``https``) and its
16
+ value is ``module:FactoryClass`` (a ``DrbFactory`` subclass). Mirrors
17
+ :class:`drb.core.factory.FactoryLoader`.
18
+ """
19
+ __instance = None
20
+ __factories = None
21
+
22
+ def __new__(cls, *args, **kwargs):
23
+ if cls.__instance is None:
24
+ cls.__instance = super(ProtocolLoader, cls).__new__(cls)
25
+ cls.__factories = cls.__load()
26
+ return cls.__instance
27
+
28
+ @classmethod
29
+ def __is_class(cls, class_name: str):
30
+ return lambda obj: inspect.isclass(obj) and class_name == obj.__name__
31
+
32
+ @classmethod
33
+ def __load(cls) -> dict:
34
+ factories = {}
35
+ for ep in drb.utils.plugins.get_entry_points('drb.protocol'):
36
+ if ep.name in factories:
37
+ continue
38
+ m, c = ep.value.split(':')
39
+ try:
40
+ module = importlib.import_module(m)
41
+ for _, obj in inspect.getmembers(module, cls.__is_class(c)):
42
+ if obj != DrbFactory and issubclass(obj, DrbFactory):
43
+ factories[ep.name] = obj()
44
+ else:
45
+ logging.getLogger().warning(
46
+ f'Failed to load protocol "{ep.name}": '
47
+ f'invalid factory: {ep.value}')
48
+ except ModuleNotFoundError:
49
+ logging.getLogger().warning(
50
+ f'Failed to load protocol "{ep.name}": module {m} '
51
+ f'not found')
52
+ return factories
53
+
54
+ def get_schemes(self) -> set:
55
+ return set(self.__factories.keys())
56
+
57
+ def get_factory(self, scheme: str) -> Optional[DrbFactory]:
58
+ return self.__factories.get(scheme, None)
@@ -49,6 +49,40 @@ class NamespaceSignature(Signature):
49
49
  return 'namespace'
50
50
 
51
51
 
52
+ class NameTestSignature(Signature):
53
+ """Allowing to check if a DRB Node name equals a specific string."""
54
+
55
+ def __init__(self, value: str):
56
+ self.__value = value
57
+
58
+ def matches(self, node: DrbNode) -> bool:
59
+ return node.name == self.__value
60
+
61
+ def to_dict(self) -> dict:
62
+ return {self.get_name(): self.__value}
63
+
64
+ @staticmethod
65
+ def get_name():
66
+ return 'nameTest'
67
+
68
+
69
+ class NamespaceTestSignature(Signature):
70
+ """Allowing to check if a DRB Node namespace equals a specific string."""
71
+
72
+ def __init__(self, value: str):
73
+ self.__value = value
74
+
75
+ def matches(self, node: DrbNode) -> bool:
76
+ return node.namespace_uri == self.__value
77
+
78
+ def to_dict(self) -> dict:
79
+ return {self.get_name(): self.__value}
80
+
81
+ @staticmethod
82
+ def get_name():
83
+ return 'namespaceTest'
84
+
85
+
52
86
  class PathSignature(Signature):
53
87
  """
54
88
  Allowing to check if a DRB Node path match a specific regex.
@@ -0,0 +1,124 @@
1
+ import re
2
+
3
+ from drb.core.node import DrbNode
4
+ from drb.core.signature import Signature
5
+
6
+
7
+ def _parent_topic(node: DrbNode):
8
+ """Resolve the parent node to its finest topic, or None."""
9
+ import drb.topics.resolver as resolver
10
+ parent = node.parent
11
+ if parent is None:
12
+ return None
13
+ try:
14
+ topic, _ = resolver.resolve(parent)
15
+ except Exception:
16
+ return None
17
+ return topic
18
+
19
+
20
+ def _topic_by_uri(uri: str, dao=None):
21
+ if dao is None:
22
+ from drb.topics.dao import ManagerDao
23
+ dao = ManagerDao()
24
+ for t in dao.get_all_drb_topics():
25
+ if t.uri == uri:
26
+ return t
27
+ return None
28
+
29
+
30
+ class ParentClassTestSignature(Signature):
31
+ """Matches when the node's parent resolves exactly to ``class_uri``."""
32
+
33
+ def __init__(self, class_uri: str):
34
+ self.__uri = class_uri
35
+
36
+ def matches(self, node: DrbNode) -> bool:
37
+ topic = _parent_topic(node)
38
+ return topic is not None and topic.uri == self.__uri
39
+
40
+ def to_dict(self) -> dict:
41
+ return {self.get_name(): self.__uri}
42
+
43
+ @staticmethod
44
+ def get_name():
45
+ return 'parentClassTest'
46
+
47
+
48
+ class ParentIsSubClassTestSignature(Signature):
49
+ """Matches when the parent's topic is a sub-class of ``class_uri``."""
50
+
51
+ def __init__(self, class_uri: str):
52
+ self.__uri = class_uri
53
+
54
+ def matches(self, node: DrbNode) -> bool:
55
+ from drb.topics.dao import ManagerDao
56
+ topic = _parent_topic(node)
57
+ if topic is None:
58
+ return False
59
+ dao = ManagerDao()
60
+ expected = _topic_by_uri(self.__uri, dao)
61
+ if expected is None:
62
+ return False
63
+ return dao.is_subclass(topic, expected)
64
+
65
+ def to_dict(self) -> dict:
66
+ return {self.get_name(): self.__uri}
67
+
68
+ @staticmethod
69
+ def get_name():
70
+ return 'parentIsSubClassTest'
71
+
72
+
73
+ class ParentClassMatchSignature(Signature):
74
+ """Matches when the parent's topic class URI matches ``regex``.
75
+
76
+ Parity with Java ``DrbCortexItemSignature``: the regex is tested against
77
+ the parent topic's **class URI**, not its label.
78
+ """
79
+
80
+ def __init__(self, regex: str):
81
+ self.__regex = regex
82
+
83
+ def matches(self, node: DrbNode) -> bool:
84
+ topic = _parent_topic(node)
85
+ return topic is not None and re.match(self.__regex, topic.uri) \
86
+ is not None
87
+
88
+ def to_dict(self) -> dict:
89
+ return {self.get_name(): self.__regex}
90
+
91
+ @staticmethod
92
+ def get_name():
93
+ return 'parentClassMatch'
94
+
95
+
96
+ class ParentIsSubClassMatchSignature(Signature):
97
+ """Matches when a topic whose **class URI** matches ``regex`` is a
98
+ super-class of the parent's topic.
99
+
100
+ Parity with Java ``DrbCortexItemSignature``: the regex is tested against
101
+ the candidate topic's **class URI**, not its label.
102
+ """
103
+
104
+ def __init__(self, regex: str):
105
+ self.__regex = regex
106
+
107
+ def matches(self, node: DrbNode) -> bool:
108
+ topic = _parent_topic(node)
109
+ if topic is None:
110
+ return False
111
+ from drb.topics.dao import ManagerDao
112
+ dao = ManagerDao()
113
+ for candidate in dao.get_all_drb_topics():
114
+ if re.match(self.__regex, candidate.uri) is not None \
115
+ and dao.is_subclass(topic, candidate):
116
+ return True
117
+ return False
118
+
119
+ def to_dict(self) -> dict:
120
+ return {self.get_name(): self.__regex}
121
+
122
+ @staticmethod
123
+ def get_name():
124
+ return 'parentIsSubClassMatch'
@@ -24,6 +24,38 @@ logger = logging.getLogger('DrbTopic')
24
24
  _HTTP_TIMEOUT = (10, 60)
25
25
 
26
26
 
27
+ # Topic descriptor file names looked up in a topic package, in priority
28
+ # order: ontology descriptors (TTL/OWL) first, legacy YAML cortex last. This
29
+ # lets a package ship its topic as TTL (the knowledge-base format) while YAML
30
+ # keeps working during the migration.
31
+ _DESCRIPTOR_NAMES = ('cortex.ttl', 'cortex.owl', 'cortex.yml', 'cortex.yaml')
32
+
33
+
34
+ def _find_topic_descriptor(directory: str):
35
+ """Return the topic descriptor path in ``directory`` (TTL/OWL preferred
36
+ over YAML), or None if none is present."""
37
+ for name in _DESCRIPTOR_NAMES:
38
+ path = os.path.join(directory, name)
39
+ if os.path.exists(path):
40
+ return path
41
+ return None
42
+
43
+
44
+ def _implementation_available(topic: DrbTopic) -> bool:
45
+ """Whether the topic's driver implementation is installed.
46
+
47
+ A topic with no ``factory`` needs no driver (abstract/container topic).
48
+ Otherwise its ``implementationIdentifier`` (``topic.factory``) must match
49
+ an installed ``drb.driver`` entry point: a descriptor (TTL/OWL/YAML) is not
50
+ enough — the implementation is required.
51
+ """
52
+ if topic.factory is None:
53
+ return True
54
+ # Lazy import to avoid any import cycle with drb.core.factory.
55
+ from drb.core.factory import FactoryLoader
56
+ return topic.factory in FactoryLoader().get_factories()
57
+
58
+
27
59
  def _load_all_drb_topic_dao() -> Dict[uuid.UUID, DrbTopicDao]:
28
60
  """
29
61
  Loads topics defined in the Python context
@@ -39,11 +71,23 @@ def _load_all_drb_topic_dao() -> Dict[uuid.UUID, DrbTopicDao]:
39
71
  logger.warning(f'Invalid entry point {ep.name}: {ex.msg}')
40
72
  continue
41
73
 
74
+ path = _find_topic_descriptor(module.__path__[0])
75
+ if path is None:
76
+ logger.warning(
77
+ f'No topic descriptor (cortex.ttl/.owl/.yml) for {ep.name}')
78
+ continue
79
+
42
80
  try:
43
- path = os.path.join(module.__path__[0], 'cortex.yml')
44
81
  dao = DaoFactory().create(path)
45
82
  topics = dao.read_all()
46
83
  for topic in topics:
84
+ if not _implementation_available(topic):
85
+ logger.warning(
86
+ "Topic %s (%s) skipped: no installed driver "
87
+ "implementation '%s' (drb.driver). Descriptor present "
88
+ "but implementation required.",
89
+ topic.label, topic.uri, topic.factory)
90
+ continue
47
91
  if topic.id in result:
48
92
  logger.warning(
49
93
  f'Topic definition conflict: id ({topic.id}) defined '
@@ -165,6 +209,24 @@ class ManagerDao:
165
209
 
166
210
  raise DrbException('No DrbTopic found')
167
211
 
212
+ def get_dao(self, identifier: uuid.UUID) -> DrbTopicDao:
213
+ """
214
+ Retrieves the DAO that loaded a topic.
215
+
216
+ Lets callers (e.g. add-ons) reach the source of a topic without the
217
+ manager knowing about their concern: for an RDF topic the returned
218
+ ``RDFDao`` exposes ``graph``, the ontology graph it was read from.
219
+
220
+ Parameters:
221
+ identifier (UUID): topic UUID
222
+ Returns:
223
+ DrbTopicDao: the DAO that loaded the topic identified by the
224
+ given UUID.
225
+ """
226
+ if identifier in self.__topic_dao.keys():
227
+ return self.__topic_dao[identifier]
228
+ raise DrbException('No DrbTopic found')
229
+
168
230
  def is_subclass(self, actual: DrbTopic, expected: DrbTopic) -> bool:
169
231
  """
170
232
  Check if a topic is subclass of another.
@@ -264,6 +326,13 @@ class ManagerDao:
264
326
  self.__added_dao.append(dao)
265
327
  topics = dao.read_all()
266
328
  for topic in topics:
329
+ if not _implementation_available(topic):
330
+ logger.warning(
331
+ "Topic %s (%s) skipped: no installed driver "
332
+ "implementation '%s' (drb.driver). Descriptor present "
333
+ "but implementation required.",
334
+ topic.label, topic.uri, topic.factory)
335
+ continue
267
336
  self.__topic_dao[topic.id] = dao
268
337
  self.__update_topics()
269
338
 
@@ -280,6 +349,13 @@ class ManagerDao:
280
349
  """
281
350
  self.__added_dao.append(dao)
282
351
  for topic in dao.read_all():
352
+ if not _implementation_available(topic):
353
+ logger.warning(
354
+ "Topic %s (%s) skipped: no installed driver "
355
+ "implementation '%s' (drb.driver). Descriptor present "
356
+ "but implementation required.",
357
+ topic.label, topic.uri, topic.factory)
358
+ continue
283
359
  self.__topic_dao[topic.id] = dao
284
360
  self.__update_topics()
285
361
  return dao
@@ -115,13 +115,31 @@ class RDFDao(DrbTopicDao):
115
115
 
116
116
  result = self.graph.query(f'''
117
117
  PREFIX drb: <http://www.gael.fr/drb#>
118
- SELECT ?xquery ?name
118
+ SELECT ?name ?nameTest ?namespace ?namespaceTest
119
+ ?xquery ?python ?pythonScript
120
+ ?parentClassMatch ?parentClassTest
121
+ ?parentIsSubClassMatch ?parentIsSubClassTest
119
122
  WHERE {{
120
123
  ?Class a owl:Class .
121
124
  FILTER(?Class = <{topic.uri}>)
122
125
  ?Class drb:signature ?signature .
123
- OPTIONAL {{ ?signature drb:xqueryTest ?xquery . }}
124
126
  OPTIONAL {{ ?signature drb:nameMatch ?name . }}
127
+ OPTIONAL {{ ?signature drb:nameTest ?nameTest . }}
128
+ OPTIONAL {{ ?signature drb:namespaceMatch ?namespace . }}
129
+ OPTIONAL {{ ?signature drb:namespaceTest ?namespaceTest . }}
130
+ OPTIONAL {{ ?signature drb:xqueryTest ?xquery . }}
131
+ OPTIONAL {{ ?signature drb:pythonTest ?python . }}
132
+ OPTIONAL {{ ?signature drb:pythonScript ?pythonScript . }}
133
+ OPTIONAL {{
134
+ ?signature drb:parentClassMatch ?parentClassMatch . }}
135
+ OPTIONAL {{
136
+ ?signature drb:parentClassTest ?parentClassTest . }}
137
+ OPTIONAL {{
138
+ ?signature drb:parentIsSubClassMatch
139
+ ?parentIsSubClassMatch . }}
140
+ OPTIONAL {{
141
+ ?signature drb:parentIsSubClassTest
142
+ ?parentIsSubClassTest . }}
125
143
  }}
126
144
  GROUP BY ?signature
127
145
  ''')
@@ -131,7 +149,16 @@ class RDFDao(DrbTopicDao):
131
149
  for k in row.asdict().keys():
132
150
  v = row[k]
133
151
  if v is not None:
134
- signature[k] = v.toPython()
152
+ if k == 'pythonScript':
153
+ # drb:pythonScript is the deprecated spelling of
154
+ # drb:pythonTest (kept for backward-compat with KB
155
+ # descriptors); both map to the python PythonSignature.
156
+ logging.getLogger('drb.topics.dao.rdf_dao').warning(
157
+ 'drb:pythonScript is deprecated; '
158
+ 'use drb:pythonTest')
159
+ signature['python'] = v.toPython()
160
+ else:
161
+ signature[k] = v.toPython()
135
162
  if signature:
136
163
  signatures.append(parse_signature(signature))
137
164
 
@@ -10,6 +10,7 @@ from typing import List, Optional, Tuple, Union
10
10
 
11
11
  from drb.core.node import DrbNode
12
12
  from drb.core.factory import DrbFactory, FactoryLoader
13
+ from drb.core.protocol import ProtocolLoader
13
14
  from drb.topics.topic import DrbTopic, TopicCategory
14
15
  from drb.exceptions.core import DrbFactoryException, DrbException
15
16
  from drb.nodes.url_node import UrlNode
@@ -30,14 +31,38 @@ def _is_remote_url(parsed_path):
30
31
  def forced_factory(topic: DrbTopic, node: DrbNode) -> Optional[DrbNode]:
31
32
  if topic.forced and topic.factory is not None:
32
33
  try:
33
- new_node = FactoryLoader().get_factory(
34
- topic.factory).create(node)
34
+ new_node = _create_via_factory(topic, node)
35
35
  except Exception as e:
36
36
  return node
37
37
  return new_node
38
38
  return node
39
39
 
40
40
 
41
+ def _create_via_factory(topic: DrbTopic, node: DrbNode) -> DrbNode:
42
+ """Create the child node via the topic's driver factory.
43
+
44
+ Raises a clear :class:`DrbException` when no ``drb.driver`` implementation
45
+ is installed for ``topic.factory`` — replacing the opaque ``None.create``
46
+ ``AttributeError``.
47
+ """
48
+ factory = FactoryLoader().get_factory(topic.factory)
49
+ if factory is None:
50
+ raise DrbException(
51
+ f"No driver implementation '{topic.factory}' installed for "
52
+ f"topic '{topic.label}' ({topic.uri})")
53
+ return factory.create(node)
54
+
55
+
56
+ def _topic_by_factory(name: str):
57
+ """The PROTOCOL topic whose implementationIdentifier == name, or None."""
58
+ from drb.topics.dao import ManagerDao
59
+ for topic in ManagerDao().get_drb_topics_by_category(
60
+ TopicCategory.PROTOCOL):
61
+ if topic.factory == name:
62
+ return topic
63
+ return None
64
+
65
+
41
66
  class _DrbFactoryResolver(DrbFactory):
42
67
  """ The factory resolver
43
68
 
@@ -74,6 +99,29 @@ class _DrbFactoryResolver(DrbFactory):
74
99
  return protocol
75
100
  return None
76
101
 
102
+ def __dispatch_scheme(self, node):
103
+ """Dispatch a root URL by its scheme via drb.protocol, then apply a
104
+ ``+<name>`` forced driver if present. Returns (topic, node) or None
105
+ when the base scheme is not registered (caller falls back to
106
+ signature matching)."""
107
+ scheme = node.path.scheme
108
+ if not scheme:
109
+ return None
110
+ parts = scheme.split('+', 1)
111
+ base = parts[0]
112
+ forced = parts[1] if len(parts) > 1 else None
113
+ base_factory = ProtocolLoader().get_factory(base)
114
+ if base_factory is None:
115
+ return None
116
+ created = base_factory.create(node)
117
+ if forced is None:
118
+ return _topic_by_factory(base), created
119
+ forced_factory = FactoryLoader().get_factory(forced)
120
+ if forced_factory is None:
121
+ raise DrbFactoryException(
122
+ f'No driver registered for forced scheme "+{forced}"')
123
+ return _topic_by_factory(forced), forced_factory.create(created)
124
+
77
125
  def __retrieve_container(
78
126
  self, node: DrbNode) -> Tuple[Optional[DrbTopic], DrbNode]:
79
127
  """
@@ -122,8 +170,7 @@ class _DrbFactoryResolver(DrbFactory):
122
170
  if topic.matches(node):
123
171
  n = node
124
172
  if topic.factory is not None:
125
- n = FactoryLoader().get_factory(
126
- topic.factory).create(node)
173
+ n = _create_via_factory(topic, node)
127
174
  return self.__finest_drb_topic(n, topic)
128
175
  return finest, node
129
176
 
@@ -230,23 +277,23 @@ class _DrbFactoryResolver(DrbFactory):
230
277
  node = source
231
278
  protocol = None
232
279
  if node.parent is None:
233
-
234
- protocol = self.__retrieve_protocol(node)
235
- if protocol is None:
236
- raise DrbFactoryException(f'Cannot resolve: {source}')
237
- node = FactoryLoader().get_factory(
238
- protocol.factory).create(node)
280
+ dispatched = self.__dispatch_scheme(node)
281
+ if dispatched is not None:
282
+ protocol, node = dispatched
283
+ else:
284
+ protocol = self.__retrieve_protocol(node)
285
+ if protocol is None:
286
+ raise DrbFactoryException(f'Cannot resolve: {source}')
287
+ node = _create_via_factory(protocol, node)
239
288
 
240
289
  container, node = self.__retrieve_container(node)
241
290
  if container is not None and container.factory is not None:
242
- node = FactoryLoader().get_factory(
243
- container.factory).create(node)
291
+ node = _create_via_factory(container, node)
244
292
 
245
293
  formatting = self.__retrieve_formatting(node)
246
294
  if formatting is not None:
247
295
  if formatting.factory is not None:
248
- node = FactoryLoader().get_factory(formatting.factory).create(
249
- node)
296
+ node = _create_via_factory(formatting, node)
250
297
  t, n = self.__finest_drb_topic(node, formatting)
251
298
  elif container is not None:
252
299
  t, n = container, node
@@ -284,7 +331,7 @@ class _DrbFactoryResolver(DrbFactory):
284
331
  if result is not None:
285
332
  topic, node = result
286
333
  if topic.factory is not None:
287
- node = FactoryLoader().get_factory(topic.factory).create(node)
334
+ node = _create_via_factory(topic, node)
288
335
  return topic, node
289
336
  return self._basic_resolve(node)
290
337
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drb
3
- Version: 2.3.4
3
+ Version: 2.3.8
4
4
  Summary: Data Request Broker
5
5
  Author: GAEL Systems
6
6
  Author-email: drb-python@gael.fr
@@ -54,22 +54,6 @@ Once successfully run, the command line prompt is updated with the virtual envir
54
54
  Once the virtual environment installed, the dependencies shall be downloaded:
55
55
  ```commandline
56
56
  (venv) $~> pip install -r requirements.txt --no-cache-dir
57
- Collecting pip==21.1.2
58
- Downloading pip-21.1.2-py3-none-any.whl (1.5 MB)
59
- |████████████████████████████████| 1.5 MB 54 kB/s
60
- Collecting setuptools==57.0.0
61
- Downloading setuptools-57.0.0-py3-none-any.whl (821 kB)
62
- |████████████████████████████████| 821 kB 73 kB/s
63
- Installing collected packages: pip, setuptools
64
- Attempting uninstall: pip
65
- Found existing installation: pip 20.0.2
66
- Uninstalling pip-20.0.2:
67
- Successfully uninstalled pip-20.0.2
68
- Attempting uninstall: setuptools
69
- Found existing installation: setuptools 44.0.0
70
- Uninstalling setuptools-44.0.0:
71
- Successfully uninstalled setuptools-44.0.0
72
- Successfully installed pip-21.1.2 setuptools-57.0.0
73
57
  ```
74
58
  At this stage the environment is installed.
75
59