drb 2.7.2__tar.gz → 2.8.0__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 (130) hide show
  1. {drb-2.7.2/drb.egg-info → drb-2.8.0}/PKG-INFO +1 -3
  2. {drb-2.7.2 → drb-2.8.0}/drb/core/_version.py +3 -3
  3. {drb-2.7.2 → drb-2.8.0}/drb/core/impls.py +11 -8
  4. {drb-2.7.2 → drb-2.8.0}/drb/nodes/abstract_node.py +22 -15
  5. {drb-2.7.2 → drb-2.8.0}/drb/nodes/logical_node.py +1 -0
  6. {drb-2.7.2 → drb-2.8.0}/drb/nodes/url_node.py +10 -7
  7. {drb-2.7.2 → drb-2.8.0}/drb/topics/dao/manager_dao.py +14 -8
  8. {drb-2.7.2 → drb-2.8.0}/drb/topics/dao/rdf_dao.py +66 -10
  9. {drb-2.7.2 → drb-2.8.0}/drb/topics/resolver.py +32 -4
  10. drb-2.8.0/drb/utils/plugins.py +9 -0
  11. {drb-2.7.2 → drb-2.8.0/drb.egg-info}/PKG-INFO +1 -3
  12. {drb-2.7.2 → drb-2.8.0}/drb.egg-info/SOURCES.txt +3 -3
  13. {drb-2.7.2 → drb-2.8.0}/drb.egg-info/requires.txt +0 -2
  14. {drb-2.7.2 → drb-2.8.0}/requirements.txt +0 -2
  15. {drb-2.7.2 → drb-2.8.0}/setup.cfg +0 -3
  16. {drb-2.7.2 → drb-2.8.0}/tests/test_abstract_node.py +29 -0
  17. {drb-2.7.2 → drb-2.8.0}/tests/test_implementation_manager.py +10 -0
  18. {drb-2.7.2 → drb-2.8.0}/tests/test_logical_node.py +9 -1
  19. {drb-2.7.2 → drb-2.8.0}/tests/test_manager_dao.py +74 -0
  20. drb-2.8.0/tests/test_named_child_no_sibling_resolution.py +60 -0
  21. drb-2.8.0/tests/test_rdf_dao_robustness.py +105 -0
  22. drb-2.8.0/tests/test_resolver_factoryless_refinement.py +75 -0
  23. {drb-2.7.2 → drb-2.8.0}/tests/test_url_node.py +45 -0
  24. drb-2.7.2/drb/topics/dao/topic_schema.yml +0 -95
  25. drb-2.7.2/drb/topics/dao/xml_dao.py +0 -184
  26. drb-2.7.2/drb/utils/plugins.py +0 -21
  27. drb-2.7.2/tests/test_xml_dao.py +0 -71
  28. {drb-2.7.2 → drb-2.8.0}/LICENCE.txt +0 -0
  29. {drb-2.7.2 → drb-2.8.0}/MANIFEST.in +0 -0
  30. {drb-2.7.2 → drb-2.8.0}/README.md +0 -0
  31. {drb-2.7.2 → drb-2.8.0}/drb/addons/addon.py +0 -0
  32. {drb-2.7.2 → drb-2.8.0}/drb/core/__init__.py +0 -0
  33. {drb-2.7.2 → drb-2.8.0}/drb/core/auth.py +0 -0
  34. {drb-2.7.2 → drb-2.8.0}/drb/core/context.py +0 -0
  35. {drb-2.7.2 → drb-2.8.0}/drb/core/cortex_validator.py +0 -0
  36. {drb-2.7.2 → drb-2.8.0}/drb/core/events.py +0 -0
  37. {drb-2.7.2 → drb-2.8.0}/drb/core/factory.py +0 -0
  38. {drb-2.7.2 → drb-2.8.0}/drb/core/item.py +0 -0
  39. {drb-2.7.2 → drb-2.8.0}/drb/core/node.py +0 -0
  40. {drb-2.7.2 → drb-2.8.0}/drb/core/path.py +0 -0
  41. {drb-2.7.2 → drb-2.8.0}/drb/core/predicate.py +0 -0
  42. {drb-2.7.2 → drb-2.8.0}/drb/core/signature.py +0 -0
  43. {drb-2.7.2 → drb-2.8.0}/drb/drivers/file/__init__.py +0 -0
  44. {drb-2.7.2 → drb-2.8.0}/drb/drivers/file/cortex.ttl +0 -0
  45. {drb-2.7.2 → drb-2.8.0}/drb/drivers/file/file.py +0 -0
  46. {drb-2.7.2 → drb-2.8.0}/drb/drivers/xml/__init__.py +0 -0
  47. {drb-2.7.2 → drb-2.8.0}/drb/drivers/xml/cortex.ttl +0 -0
  48. {drb-2.7.2 → drb-2.8.0}/drb/drivers/xml/factory.py +0 -0
  49. {drb-2.7.2 → drb-2.8.0}/drb/drivers/xml/node.py +0 -0
  50. {drb-2.7.2 → drb-2.8.0}/drb/exceptions/core.py +0 -0
  51. {drb-2.7.2 → drb-2.8.0}/drb/exceptions/file.py +0 -0
  52. {drb-2.7.2 → drb-2.8.0}/drb/keyring/bitwarden/__init__.py +0 -0
  53. {drb-2.7.2 → drb-2.8.0}/drb/keyring/bitwarden/bitwarden.py +0 -0
  54. {drb-2.7.2 → drb-2.8.0}/drb/nodes/mutable_node.py +0 -0
  55. {drb-2.7.2 → drb-2.8.0}/drb/signatures/core.py +0 -0
  56. {drb-2.7.2 → drb-2.8.0}/drb/signatures/hierarchy.py +0 -0
  57. {drb-2.7.2 → drb-2.8.0}/drb/signatures/xquery.py +0 -0
  58. {drb-2.7.2 → drb-2.8.0}/drb/topics/dao/__init__.py +0 -0
  59. {drb-2.7.2 → drb-2.8.0}/drb/topics/dao/topic_dao.py +0 -0
  60. {drb-2.7.2 → drb-2.8.0}/drb/topics/file/__init__.py +0 -0
  61. {drb-2.7.2 → drb-2.8.0}/drb/topics/file/cortex.ttl +0 -0
  62. {drb-2.7.2 → drb-2.8.0}/drb/topics/topic.py +0 -0
  63. {drb-2.7.2 → drb-2.8.0}/drb/topics/xml/__init__.py +0 -0
  64. {drb-2.7.2 → drb-2.8.0}/drb/topics/xml/cortex.ttl +0 -0
  65. {drb-2.7.2 → drb-2.8.0}/drb/utils/__init__.py +0 -0
  66. {drb-2.7.2 → drb-2.8.0}/drb/utils/drb_python_script.py +0 -0
  67. {drb-2.7.2 → drb-2.8.0}/drb/utils/io.py +0 -0
  68. {drb-2.7.2 → drb-2.8.0}/drb/utils/url_security.py +0 -0
  69. {drb-2.7.2 → drb-2.8.0}/drb/utils/utils.py +0 -0
  70. {drb-2.7.2 → drb-2.8.0}/drb/xquery/XQueryLexer.py +0 -0
  71. {drb-2.7.2 → drb-2.8.0}/drb/xquery/XQueryParser.py +0 -0
  72. {drb-2.7.2 → drb-2.8.0}/drb/xquery/XQueryParserListener.py +0 -0
  73. {drb-2.7.2 → drb-2.8.0}/drb/xquery/XQueryParserVisitor.py +0 -0
  74. {drb-2.7.2 → drb-2.8.0}/drb/xquery/__init__.py +0 -0
  75. {drb-2.7.2 → drb-2.8.0}/drb/xquery/drb_xquery.py +0 -0
  76. {drb-2.7.2 → drb-2.8.0}/drb/xquery/drb_xquery_cmd.py +0 -0
  77. {drb-2.7.2 → drb-2.8.0}/drb/xquery/drb_xquery_context.py +0 -0
  78. {drb-2.7.2 → drb-2.8.0}/drb/xquery/drb_xquery_func.py +0 -0
  79. {drb-2.7.2 → drb-2.8.0}/drb/xquery/drb_xquery_item.py +0 -0
  80. {drb-2.7.2 → drb-2.8.0}/drb/xquery/drb_xquery_res_to_string.py +0 -0
  81. {drb-2.7.2 → drb-2.8.0}/drb/xquery/drb_xquery_utils.py +0 -0
  82. {drb-2.7.2 → drb-2.8.0}/drb/xquery/drb_xquery_variable.py +0 -0
  83. {drb-2.7.2 → drb-2.8.0}/drb/xquery/drb_xquery_visitor.py +0 -0
  84. {drb-2.7.2 → drb-2.8.0}/drb/xquery/exceptions.py +0 -0
  85. {drb-2.7.2 → drb-2.8.0}/drb.egg-info/dependency_links.txt +0 -0
  86. {drb-2.7.2 → drb-2.8.0}/drb.egg-info/entry_points.txt +0 -0
  87. {drb-2.7.2 → drb-2.8.0}/drb.egg-info/not-zip-safe +0 -0
  88. {drb-2.7.2 → drb-2.8.0}/drb.egg-info/top_level.txt +0 -0
  89. {drb-2.7.2 → drb-2.8.0}/pyproject.toml +0 -0
  90. {drb-2.7.2 → drb-2.8.0}/setup.py +0 -0
  91. {drb-2.7.2 → drb-2.8.0}/tests/test_addon.py +0 -0
  92. {drb-2.7.2 → drb-2.8.0}/tests/test_auth.py +0 -0
  93. {drb-2.7.2 → drb-2.8.0}/tests/test_children_unresolved.py +0 -0
  94. {drb-2.7.2 → drb-2.8.0}/tests/test_core_topic_ttl.py +0 -0
  95. {drb-2.7.2 → drb-2.8.0}/tests/test_cortex_validator.py +0 -0
  96. {drb-2.7.2 → drb-2.8.0}/tests/test_drb_node_list.py +0 -0
  97. {drb-2.7.2 → drb-2.8.0}/tests/test_drb_python_script.py +0 -0
  98. {drb-2.7.2 → drb-2.8.0}/tests/test_event.py +0 -0
  99. {drb-2.7.2 → drb-2.8.0}/tests/test_factory.py +0 -0
  100. {drb-2.7.2 → drb-2.8.0}/tests/test_factory_resolver.py +0 -0
  101. {drb-2.7.2 → drb-2.8.0}/tests/test_finest_multiparent.py +0 -0
  102. {drb-2.7.2 → drb-2.8.0}/tests/test_implementation_coupling.py +0 -0
  103. {drb-2.7.2 → drb-2.8.0}/tests/test_keyring_bitwarden.py +0 -0
  104. {drb-2.7.2 → drb-2.8.0}/tests/test_manager_get_dao.py +0 -0
  105. {drb-2.7.2 → drb-2.8.0}/tests/test_node_context.py +0 -0
  106. {drb-2.7.2 → drb-2.8.0}/tests/test_node_context_manager.py +0 -0
  107. {drb-2.7.2 → drb-2.8.0}/tests/test_node_descendant.py +0 -0
  108. {drb-2.7.2 → drb-2.8.0}/tests/test_path.py +0 -0
  109. {drb-2.7.2 → drb-2.8.0}/tests/test_protocol_dispatch.py +0 -0
  110. {drb-2.7.2 → drb-2.8.0}/tests/test_rdf_dao.py +0 -0
  111. {drb-2.7.2 → drb-2.8.0}/tests/test_rdf_dao_multi_url.py +0 -0
  112. {drb-2.7.2 → drb-2.8.0}/tests/test_rdf_dao_scheme.py +0 -0
  113. {drb-2.7.2 → drb-2.8.0}/tests/test_rdf_dao_signatures.py +0 -0
  114. {drb-2.7.2 → drb-2.8.0}/tests/test_resolver_auth.py +0 -0
  115. {drb-2.7.2 → drb-2.8.0}/tests/test_resolver_format_alias.py +0 -0
  116. {drb-2.7.2 → drb-2.8.0}/tests/test_resolver_implicit_file.py +0 -0
  117. {drb-2.7.2 → drb-2.8.0}/tests/test_resolver_recursion_guard.py +0 -0
  118. {drb-2.7.2 → drb-2.8.0}/tests/test_scheme_force_impl.py +0 -0
  119. {drb-2.7.2 → drb-2.8.0}/tests/test_scheme_force_url.py +0 -0
  120. {drb-2.7.2 → drb-2.8.0}/tests/test_signature_duplicate_warning.py +0 -0
  121. {drb-2.7.2 → drb-2.8.0}/tests/test_signature_exact.py +0 -0
  122. {drb-2.7.2 → drb-2.8.0}/tests/test_signature_hierarchy.py +0 -0
  123. {drb-2.7.2 → drb-2.8.0}/tests/test_signature_inline_flags.py +0 -0
  124. {drb-2.7.2 → drb-2.8.0}/tests/test_signatures.py +0 -0
  125. {drb-2.7.2 → drb-2.8.0}/tests/test_topic.py +0 -0
  126. {drb-2.7.2 → drb-2.8.0}/tests/test_topic_descriptor_discovery.py +0 -0
  127. {drb-2.7.2 → drb-2.8.0}/tests/test_topic_merge_crosspackage.py +0 -0
  128. {drb-2.7.2 → drb-2.8.0}/tests/test_url_security.py +0 -0
  129. {drb-2.7.2 → drb-2.8.0}/tests/test_xml_xquery_import_order.py +0 -0
  130. {drb-2.7.2 → drb-2.8.0}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drb
3
- Version: 2.7.2
3
+ Version: 2.8.0
4
4
  Summary: Data Request Broker
5
5
  Author: GAEL Systems
6
6
  Author-email: drb-python@gael.fr
@@ -12,8 +12,6 @@ Requires-Python: <3.14,>=3.11
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENCE.txt
14
14
  Requires-Dist: importlib-metadata>=4.11.4
15
- Requires-Dist: jsonschema==4.17.0
16
- Requires-Dist: PyYaml~=6.0.2
17
15
  Requires-Dist: click==8.1.3
18
16
  Requires-Dist: keyring<26,>=25.7.0
19
17
  Requires-Dist: requests<3,>=2.33.0
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2026-07-07T13:58:00+0000",
11
+ "date": "2026-07-10T13:20:21+0000",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "0ca7a30d2606890cfee27aa9c615247cf1a7be67",
15
- "version": "2.7.2"
14
+ "full-revisionid": "dad4a706c920e704103cb0a1c83b0bfbdf97a204",
15
+ "version": "2.8.0"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -26,15 +26,15 @@ class ImplementationManager:
26
26
 
27
27
  def __find_impl(self, impl: type, identifier: Optional[str]) \
28
28
  -> List[Tuple[type, Optional[str]]]:
29
+ # keys are iterated in registration order so that a multi-match
30
+ # lookup stays deterministic (first registered wins in get_impl)
29
31
  if identifier is None:
30
32
  if (impl, None) in self.__impls.keys():
31
33
  return [(impl, None)]
32
- keys = set(self.__impls.keys())
34
+ keys = self.__impls.keys()
33
35
  else:
34
- keys = set(
35
- filter(lambda x: x[1] == identifier, self.__impls.keys())
36
- )
37
- return list(filter(lambda x: issubclass(x[0], impl), keys))
36
+ keys = filter(lambda x: x[1] == identifier, self.__impls.keys())
37
+ return [key for key in keys if issubclass(key[0], impl)]
38
38
 
39
39
  def has_impl(self, impl: type, identifier: str = None) -> bool:
40
40
  """
@@ -62,14 +62,17 @@ class ImplementationManager:
62
62
  specify from which add-on retrieve the
63
63
  implementation (default: ``None``)
64
64
  Return:
65
- Callable: the expected implementation.
65
+ Callable: the expected implementation. When several registered
66
+ implementations match, the first registered one is returned,
67
+ keeping ``get_impl`` consistent with ``has_impl``.
66
68
  Raises:
67
69
  KeyError: if the given interface is not found
68
70
  """
69
71
  keys = self.__find_impl(impl, identifier)
70
- if len(keys) == 1:
72
+ if keys:
71
73
  return self.__impls[keys[0]]
72
- raise KeyError
74
+ raise KeyError(f'No implementation ({impl.__name__}, {identifier}) '
75
+ 'found')
73
76
 
74
77
  def get_capabilities(self) -> List[Tuple[type, str]]:
75
78
  """
@@ -112,8 +112,10 @@ class AbstractNode(DrbNode, abc.ABC):
112
112
  def __setitem__(self, key, value):
113
113
  if key is None:
114
114
  self._browse_children().append(value)
115
+ return
115
116
  if isinstance(key, int):
116
117
  self._browse_children()[key] = value
118
+ return
117
119
  try:
118
120
  if isinstance(key, str):
119
121
  self._browse_children()[
@@ -122,23 +124,24 @@ class AbstractNode(DrbNode, abc.ABC):
122
124
  key = get_name_namespace_index(key)
123
125
  self._browse_children()[self._get_named_child_index(
124
126
  *key)] = value
127
+ else:
128
+ raise KeyError(f'Invalid key {key}')
125
129
  except (DrbException, IndexError, TypeError) as ex:
126
130
  raise KeyError(f'Invalid key {key}') from ex
127
131
 
128
132
  def __delitem__(self, key):
129
133
  if isinstance(key, int):
130
134
  del self._browse_children()[key]
135
+ return
131
136
  try:
132
137
  if isinstance(key, str):
133
138
  del self._browse_children()[self._get_named_child_index(key)]
134
- elif isinstance(key, tuple):
135
- if len(key) == 2:
136
- key = get_name_namespace_index(key)
137
- del self._browse_children()[self._get_named_child_index(
138
- *key)]
139
- raise KeyError(f"Invalid key {key}.")
140
- else:
141
- raise KeyError(f'Invalid key {key}')
139
+ elif isinstance(key, tuple) and len(key) == 2:
140
+ key = get_name_namespace_index(key)
141
+ del self._browse_children()[self._get_named_child_index(
142
+ *key)]
143
+ else:
144
+ raise KeyError(f'Invalid key {key}')
142
145
  except (DrbException, IndexError, TypeError) as ex:
143
146
  raise KeyError(f'Invalid key {key}') from ex
144
147
 
@@ -162,14 +165,14 @@ class AbstractNode(DrbNode, abc.ABC):
162
165
  occ = occurrence
163
166
  cpt = 0
164
167
  if self.namespace_aware or namespace_uri is not None:
165
- for x in self._browse_children():
168
+ for x in self.children_unresolved:
166
169
  if x.name == name and x.namespace_uri == namespace_uri:
167
170
  if occ <= 0:
168
171
  return cpt
169
172
  occ -= 1
170
173
  cpt += 1
171
174
  else:
172
- for x in self._browse_children():
175
+ for x in self.children_unresolved:
173
176
  if x.name == name:
174
177
  return cpt
175
178
  cpt += 1
@@ -199,12 +202,16 @@ class AbstractNode(DrbNode, abc.ABC):
199
202
  IndexError: if item is out of range of found children
200
203
  DrbException: if no child following given criteria is found
201
204
  """
205
+ # Match unresolved children and let the caller resolve only the
206
+ # selected node: a targeted named lookup must not resolve unrelated
207
+ # siblings (contract: the driver owns optimized access; the core
208
+ # resolves only when necessary). drb-testbench#21.
202
209
  if self.namespace_aware or namespace_uri is not None:
203
- named_children = [x for x in self._browse_children()
210
+ named_children = [x for x in self.children_unresolved
204
211
  if x.name == name
205
212
  and x.namespace_uri == namespace_uri]
206
213
  else:
207
- named_children = [x for x in self._browse_children()
214
+ named_children = [x for x in self.children_unresolved
208
215
  if x.name == name]
209
216
  if len(named_children) <= 0:
210
217
  raise DrbException(f'No child found having name: {name} and'
@@ -225,13 +232,13 @@ class AbstractNode(DrbNode, abc.ABC):
225
232
 
226
233
  result = []
227
234
  if name is not None and namespace is not None:
228
- result = [x for x in self._browse_children()
235
+ result = [x for x in self.children_unresolved
229
236
  if x.name == name and x.namespace_uri == namespace]
230
237
  if name is None:
231
- result = [x for x in self._browse_children()
238
+ result = [x for x in self.children_unresolved
232
239
  if x.namespace_uri == namespace]
233
240
  if namespace is None:
234
- result = [x for x in self._browse_children() if x.name == name]
241
+ result = [x for x in self.children_unresolved if x.name == name]
235
242
 
236
243
  return len(result) > 0
237
244
 
@@ -139,6 +139,7 @@ class WrappedNode(MutableNode):
139
139
 
140
140
  def __imatmul__(self, other):
141
141
  self._wrapped.__imatmul__(other)
142
+ return self
142
143
 
143
144
  def __matmul__(self, other):
144
145
  return self._wrapped.__matmul__(other)
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
  import io
3
- import keyring
4
3
  import requests
5
- from requests.auth import AuthBase, HTTPBasicAuth
4
+ from requests.auth import AuthBase
6
5
  from typing import Any, Optional, List, Dict, Tuple, Union
6
+ from urllib.parse import urlparse
7
7
 
8
+ from drb.core.auth import credential_to_authbase, resolve_credential
8
9
  from drb.core.node import DrbNode
9
10
  from drb.core.path import ParsedPath, Path, parse_path
10
11
  from drb.exceptions.core import DrbException
@@ -107,11 +108,13 @@ class UrlNode(DrbNode):
107
108
  raise DrbException('UrlNode has no child')
108
109
 
109
110
  def _find_credential(self, url: str):
111
+ # explicit auth wins; otherwise resolve through the drb.core.auth
112
+ # provider chain (injected > keyring > env > anonymous),
113
+ # host-scoped like every transport driver
110
114
  if self._auth is not None:
111
115
  return self._auth
112
116
 
113
- credential = keyring.get_credential(url, None)
114
- if credential is not None:
115
- return HTTPBasicAuth(credential.username, credential.password)
116
-
117
- return None
117
+ parsed = urlparse(url)
118
+ credential = resolve_credential(self._context, parsed.scheme,
119
+ parsed.hostname or '')
120
+ return credential_to_authbase(credential)
@@ -8,7 +8,6 @@ import os
8
8
  import logging
9
9
  import importlib
10
10
  import drb.utils.plugins
11
- from jsonschema.exceptions import ValidationError
12
11
  from drb.utils.url_security import validate_url
13
12
 
14
13
  from .topic_dao import DrbTopicDao
@@ -86,8 +85,10 @@ def _load_all_drb_topic_dao() -> Dict[uuid.UUID, DrbTopicDao]:
86
85
  return result
87
86
 
88
87
  # One merged graph across all descriptors (cross-package resolution).
88
+ # skip_invalid: one broken descriptor is logged and skipped instead of
89
+ # aborting topic loading for every installed package.
89
90
  try:
90
- dao = RDFDao(paths)
91
+ dao = RDFDao(paths, skip_invalid=True)
91
92
  for topic in dao.read_all():
92
93
  if not _implementation_available(topic):
93
94
  logger.warning(
@@ -97,8 +98,8 @@ def _load_all_drb_topic_dao() -> Dict[uuid.UUID, DrbTopicDao]:
97
98
  topic.label, topic.uri, topic.factory)
98
99
  continue
99
100
  result[topic.id] = dao
100
- except (FileNotFoundError, ValidationError) as ex:
101
- logger.warning(f'Invalid topic description(s): {ex}')
101
+ except Exception as ex:
102
+ logger.error(f'Topic loading failed: {ex}')
102
103
 
103
104
  return result
104
105
 
@@ -245,9 +246,14 @@ class ManagerDao:
245
246
  return False
246
247
 
247
248
  for parent_id in actual.subClassOf:
248
-
249
- return self.is_subclass(self.get_drb_topic(parent_id),
250
- expected)
249
+ try:
250
+ parent = self.get_drb_topic(parent_id)
251
+ except DrbException:
252
+ # dangling parent reference: cannot match, try the next one
253
+ continue
254
+ if self.is_subclass(parent, expected):
255
+ return True
256
+ return False
251
257
 
252
258
  def get_parents(self, topic: DrbTopic) -> List[DrbTopic]:
253
259
 
@@ -259,7 +265,7 @@ class ManagerDao:
259
265
  for parent in topic.subClassOf:
260
266
 
261
267
  if parent in self.__topic_dao.keys():
262
- dao = self.__topic_dao[topic.id]
268
+ dao = self.__topic_dao[parent]
263
269
  parents.append(dao.read(parent))
264
270
 
265
271
  return parents
@@ -22,6 +22,8 @@ _DRB_ITEM_URI_LEGACY = "http://www.gael.fr/drb/item"
22
22
  # Default (connect, read) timeout in seconds for outbound HTTP requests.
23
23
  _HTTP_TIMEOUT = (10, 60)
24
24
 
25
+ logger = logging.getLogger(__name__)
26
+
25
27
 
26
28
  def _rdf_bool(literal) -> bool:
27
29
  """Coerce an RDF literal to bool.
@@ -52,21 +54,33 @@ class RDFDao(DrbTopicDao):
52
54
  self,
53
55
  path: Union[str, List[str]],
54
56
  auth: Optional[tuple] = None,
57
+ skip_invalid: bool = False,
55
58
  ):
56
59
  """
57
60
  Load RDF from a single path/URL or from a list of URLs (e.g. Graph
58
61
  Store), merging into one graph. Optional auth is (user, password)
59
- for HTTP Basic when loading from http/https URLs.
62
+ for HTTP Basic when loading from http/https URLs. With
63
+ ``skip_invalid`` an unreadable/unparsable source is logged and
64
+ skipped instead of aborting the whole load (used by the merged
65
+ entry-point loading, so one broken descriptor cannot take every
66
+ other package down).
60
67
  """
61
68
  self.graph = rdflib.Graph()
62
- if isinstance(path, list):
63
- for url in path:
64
- u = url.strip()
65
- if u:
66
- self._parse_one(u, auth)
67
- else:
68
- self._parse_one(path, auth)
69
+ sources = path if isinstance(path, list) else [path]
70
+ for source in sources:
71
+ src = source.strip() if isinstance(source, str) else source
72
+ if not src:
73
+ continue
74
+ if skip_invalid:
75
+ try:
76
+ self._parse_one(src, auth)
77
+ except Exception as ex:
78
+ logger.error(
79
+ 'Skipping invalid topic descriptor %s: %s', src, ex)
80
+ else:
81
+ self._parse_one(src, auth)
69
82
  self.__result = self.__query_rdf_file()
83
+ self._warn_undiscoverable_topics()
70
84
 
71
85
  def _parse_one(self, path_or_url: str, auth: Optional[tuple]) -> None:
72
86
  """Parse one path or URL into self.graph."""
@@ -138,6 +152,43 @@ class RDFDao(DrbTopicDao):
138
152
 
139
153
  return result
140
154
 
155
+ def _warn_undiscoverable_topics(self) -> None:
156
+ """Warn about topic-like subjects discovery will silently drop.
157
+
158
+ Discovery only selects classes reaching ``drb:item`` through
159
+ ``rdfs:subClassOf+``; a topic whose parent package is not loaded
160
+ (or whose parent URI is mistyped) simply vanishes. Surface those
161
+ here instead of leaving the user to wonder why a topic does not
162
+ resolve.
163
+ """
164
+ result = self.graph.query(f"""
165
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
166
+ PREFIX drb: <http://www.gael.fr/drb#>
167
+ SELECT DISTINCT ?Class
168
+ (GROUP_CONCAT(DISTINCT STR(?parent); separator=", ")
169
+ AS ?parents)
170
+ WHERE {{
171
+ {{ ?Class drb:id ?mark . }}
172
+ UNION {{ ?Class drb:signature ?mark . }}
173
+ UNION {{ ?Class drb:implementationIdentifier ?mark . }}
174
+ OPTIONAL {{ ?Class rdfs:subClassOf ?parent . }}
175
+ FILTER NOT EXISTS {{
176
+ ?Class rdfs:subClassOf+ <{DRB_ITEM_URI}> . }}
177
+ FILTER NOT EXISTS {{
178
+ ?Class rdfs:subClassOf+ <{_DRB_ITEM_URI_LEGACY}> . }}
179
+ FILTER (?Class != <{DRB_ITEM_URI}>
180
+ && ?Class != <{_DRB_ITEM_URI_LEGACY}>)
181
+ }}
182
+ GROUP BY ?Class
183
+ """)
184
+ for row in result:
185
+ parents = str(row.parents) if row.parents else None
186
+ logger.warning(
187
+ "Topic '%s' is not discoverable: its rdfs:subClassOf chain "
188
+ "does not reach drb:item (direct parents: %s). Check the "
189
+ "parent URI and that the parent package is installed.",
190
+ row.Class.toPython(), parents or 'none')
191
+
141
192
  def get_topic_signature(self, topic: DrbTopic):
142
193
 
143
194
  result = self.graph.query(f'''
@@ -281,7 +332,7 @@ class RDFDao(DrbTopicDao):
281
332
 
282
333
  continue
283
334
 
284
- raise DrbException
335
+ raise DrbException(f'No topic found for identifier: {identifier}')
285
336
 
286
337
  def find(self, search: str) -> List[DrbTopic]:
287
338
  """
@@ -312,7 +363,12 @@ class RDFDao(DrbTopicDao):
312
363
  topic = self.__generate_topic_from_rdf(r)
313
364
  topics.append(topic)
314
365
 
315
- except TypeError:
366
+ except (TypeError, ValueError) as ex:
367
+ # e.g. a non-UUID drb:id or an unknown drb:category: skip
368
+ # the row but say so, a silent drop is undebuggable
369
+ logger.warning(
370
+ "Skipping malformed topic row '%s': %s",
371
+ r.Class.toPython(), ex)
316
372
  continue
317
373
 
318
374
  return topics
@@ -94,6 +94,31 @@ def _topic_by_factory(name: str):
94
94
  return None
95
95
 
96
96
 
97
+ def _driver_topic(topic: DrbTopic) -> Optional[DrbTopic]:
98
+ """Nearest topic in the ``subClassOf`` chain (``topic`` first) that
99
+ declares a driver ``factory``.
100
+
101
+ A semantic refinement (e.g. a mission-specific container/formatting topic
102
+ such as ``Sentinel-2 …: Zip``) may declare no ``factory`` of its own; the
103
+ node must still be built by the driver of the nearest ancestor that has
104
+ one -- the refinement only relabels it, it does not rebuild it. Returns
105
+ ``None`` when no topic in the chain declares a factory.
106
+ """
107
+ by_id = {t.id: t for t in ManagerDao().get_all_drb_topics()}
108
+ seen = set()
109
+ queue = [topic]
110
+ while queue:
111
+ current = queue.pop(0)
112
+ if current is None or current.id in seen:
113
+ continue
114
+ seen.add(current.id)
115
+ if current.factory is not None:
116
+ return current
117
+ for parent_id in (current.subClassOf or []):
118
+ queue.append(by_id.get(parent_id))
119
+ return None
120
+
121
+
97
122
  def _topic_by_scheme_any(scheme: str):
98
123
  """The topic (any category) declaring ``scheme`` in its ``drb:scheme``.
99
124
 
@@ -477,13 +502,16 @@ class _DrbFactoryResolver(DrbFactory):
477
502
  node = _create_via_factory(protocol, node)
478
503
 
479
504
  container, node = self.__retrieve_container(node)
480
- if container is not None and container.factory is not None:
481
- node = _create_via_factory(container, node)
505
+ if container is not None:
506
+ driver = _driver_topic(container)
507
+ if driver is not None:
508
+ node = _create_via_factory(driver, node)
482
509
 
483
510
  formatting = self.__retrieve_formatting(node)
484
511
  if formatting is not None:
485
- if formatting.factory is not None:
486
- node = _create_via_factory(formatting, node)
512
+ driver = _driver_topic(formatting)
513
+ if driver is not None:
514
+ node = _create_via_factory(driver, node)
487
515
  t, n = self.__finest_drb_topic(node, formatting)
488
516
  elif container is not None:
489
517
  t, n = container, node
@@ -0,0 +1,9 @@
1
+ from typing import Iterable
2
+ import importlib.metadata
3
+
4
+
5
+ def get_entry_points(name: str) -> Iterable[importlib.metadata.EntryPoint]:
6
+ eps = importlib.metadata.entry_points()
7
+ if name in eps.groups:
8
+ return eps.select(group=name)
9
+ return []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drb
3
- Version: 2.7.2
3
+ Version: 2.8.0
4
4
  Summary: Data Request Broker
5
5
  Author: GAEL Systems
6
6
  Author-email: drb-python@gael.fr
@@ -12,8 +12,6 @@ Requires-Python: <3.14,>=3.11
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENCE.txt
14
14
  Requires-Dist: importlib-metadata>=4.11.4
15
- Requires-Dist: jsonschema==4.17.0
16
- Requires-Dist: PyYaml~=6.0.2
17
15
  Requires-Dist: click==8.1.3
18
16
  Requires-Dist: keyring<26,>=25.7.0
19
17
  Requires-Dist: requests<3,>=2.33.0
@@ -51,8 +51,6 @@ drb/topics/dao/__init__.py
51
51
  drb/topics/dao/manager_dao.py
52
52
  drb/topics/dao/rdf_dao.py
53
53
  drb/topics/dao/topic_dao.py
54
- drb/topics/dao/topic_schema.yml
55
- drb/topics/dao/xml_dao.py
56
54
  drb/topics/file/__init__.py
57
55
  drb/topics/file/cortex.ttl
58
56
  drb/topics/xml/__init__.py
@@ -96,6 +94,7 @@ tests/test_keyring_bitwarden.py
96
94
  tests/test_logical_node.py
97
95
  tests/test_manager_dao.py
98
96
  tests/test_manager_get_dao.py
97
+ tests/test_named_child_no_sibling_resolution.py
99
98
  tests/test_node_context.py
100
99
  tests/test_node_context_manager.py
101
100
  tests/test_node_descendant.py
@@ -103,9 +102,11 @@ tests/test_path.py
103
102
  tests/test_protocol_dispatch.py
104
103
  tests/test_rdf_dao.py
105
104
  tests/test_rdf_dao_multi_url.py
105
+ tests/test_rdf_dao_robustness.py
106
106
  tests/test_rdf_dao_scheme.py
107
107
  tests/test_rdf_dao_signatures.py
108
108
  tests/test_resolver_auth.py
109
+ tests/test_resolver_factoryless_refinement.py
109
110
  tests/test_resolver_format_alias.py
110
111
  tests/test_resolver_implicit_file.py
111
112
  tests/test_resolver_recursion_guard.py
@@ -121,5 +122,4 @@ tests/test_topic_descriptor_discovery.py
121
122
  tests/test_topic_merge_crosspackage.py
122
123
  tests/test_url_node.py
123
124
  tests/test_url_security.py
124
- tests/test_xml_dao.py
125
125
  tests/test_xml_xquery_import_order.py
@@ -1,6 +1,4 @@
1
1
  importlib-metadata>=4.11.4
2
- jsonschema==4.17.0
3
- PyYaml~=6.0.2
4
2
  click==8.1.3
5
3
  keyring<26,>=25.7.0
6
4
  requests<3,>=2.33.0
@@ -1,6 +1,4 @@
1
1
  importlib-metadata>=4.11.4
2
- jsonschema==4.17.0
3
- PyYaml~=6.0.2
4
2
  click==8.1.3
5
3
  keyring>=25.7.0,<26
6
4
  requests>=2.33.0,<3
@@ -42,11 +42,8 @@ drb.signature =
42
42
  parentIsSubClassTest = drb.signatures.hierarchy:ParentIsSubClassTestSignature
43
43
  parentClassMatch = drb.signatures.hierarchy:ParentClassMatchSignature
44
44
  parentIsSubClassMatch = drb.signatures.hierarchy:ParentIsSubClassMatchSignature
45
- drb.auth =
46
45
 
47
46
  [options.package_data]
48
- drb.topics.dao =
49
- topic_schema.yml
50
47
  drb.topics.xml =
51
48
  cortex.ttl
52
49
  drb.topics.file =
@@ -178,6 +178,35 @@ class TestDrbNode(unittest.TestCase):
178
178
  del node['Test_0']
179
179
  self.assertEqual(len(node), 0)
180
180
 
181
+ def test_delitem_name_namespace_tuple(self):
182
+ # drb-testbench#23: deleting by (name, namespace) raised KeyError
183
+ # AFTER the child was actually removed
184
+ node = DrbLogicalNode('container')
185
+ node.append_child(DrbLogicalNode('child', namespace_uri='ns'))
186
+ node.append_child(DrbLogicalNode('other'))
187
+
188
+ del node[('child', 'ns')]
189
+
190
+ self.assertEqual(1, len(node))
191
+ self.assertEqual('other', node[0].name)
192
+
193
+ def test_delitem_invalid_key_type(self):
194
+ # drb-testbench#23: an unsupported key type was silently ignored
195
+ node = DrbLogicalNode('container')
196
+ node.append_child(DrbLogicalNode('child'))
197
+
198
+ with self.assertRaises(KeyError):
199
+ del node[1.5]
200
+ self.assertEqual(1, len(node))
201
+
202
+ def test_setitem_invalid_key_type(self):
203
+ # drb-testbench#23: an unsupported key type was silently ignored
204
+ node = DrbLogicalNode('container')
205
+
206
+ with self.assertRaises(KeyError):
207
+ node[1.5] = DrbLogicalNode('x')
208
+ self.assertEqual(0, len(node))
209
+
181
210
  def test_namespace_11(self):
182
211
  # Case 1.1: node with ns / user access with ns / aware = True
183
212
  root = DrbLogicalNode("root")
@@ -66,6 +66,16 @@ class TestImplManager(unittest.TestCase):
66
66
  with self.assertRaises(KeyError):
67
67
  mng.get_impl(dict)
68
68
 
69
+ def test_get_impl_ambiguous_returns_first_registered(self):
70
+ # drb-testbench#24: has_impl answered True but get_impl raised
71
+ # KeyError when several impls matched; first registered now wins
72
+ mng = ImplementationManager()
73
+ mng.add_impl(str, 'a', function_1)
74
+ mng.add_impl(str, 'b', function_2)
75
+
76
+ self.assertTrue(mng.has_impl(str))
77
+ self.assertEqual(function_1, mng.get_impl(str))
78
+
69
79
  def test_get_capabilities(self):
70
80
  mng = ImplementationManager()
71
81
  self.assertEqual([], mng.get_capabilities())
@@ -6,7 +6,7 @@ import operator
6
6
  from functools import partial
7
7
  from drb.core import DrbNode, Predicate, ParsedPath
8
8
  from drb.core.path import parse_path
9
- from drb.nodes.logical_node import DrbLogicalNode
9
+ from drb.nodes.logical_node import DrbLogicalNode, WrappedNode
10
10
  from drb.exceptions.core import DrbException
11
11
 
12
12
 
@@ -295,6 +295,14 @@ class TestLogicalNode(unittest.TestCase):
295
295
  node = DrbLogicalNode(source)
296
296
  node.close()
297
297
 
298
+ def test_wrapped_node_imatmul_returns_node(self):
299
+ # drb-testbench#23: WrappedNode.__imatmul__ returned None, so
300
+ # `wrapped @= ...` rebound the variable to None
301
+ wrapped = WrappedNode(DrbLogicalNode('base'))
302
+ wrapped @= ('attr', 'value')
303
+ self.assertIsNotNone(wrapped)
304
+ self.assertEqual('value', wrapped @ 'attr')
305
+
298
306
  def test_impl(self):
299
307
  source = "/path/to/data/node"
300
308
  node = DrbLogicalNode(source)