cmem-cmemc 24.3.3__py3-none-any.whl → 25.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. cmem_cmemc/__init__.py +1 -160
  2. cmem_cmemc/cli.py +138 -0
  3. cmem_cmemc/command.py +36 -0
  4. cmem_cmemc/commands/admin.py +7 -6
  5. cmem_cmemc/commands/client.py +4 -3
  6. cmem_cmemc/commands/config.py +3 -2
  7. cmem_cmemc/commands/dataset.py +18 -17
  8. cmem_cmemc/commands/graph.py +20 -22
  9. cmem_cmemc/commands/manual.py +56 -0
  10. cmem_cmemc/commands/metrics.py +11 -11
  11. cmem_cmemc/commands/migration.py +4 -3
  12. cmem_cmemc/commands/project.py +10 -10
  13. cmem_cmemc/commands/python.py +29 -2
  14. cmem_cmemc/commands/query.py +31 -14
  15. cmem_cmemc/commands/resource.py +4 -3
  16. cmem_cmemc/commands/scheduler.py +4 -4
  17. cmem_cmemc/commands/store.py +2 -2
  18. cmem_cmemc/commands/user.py +10 -9
  19. cmem_cmemc/commands/validation.py +2 -2
  20. cmem_cmemc/commands/variable.py +1 -1
  21. cmem_cmemc/commands/vocabulary.py +11 -10
  22. cmem_cmemc/commands/workflow.py +18 -18
  23. cmem_cmemc/completion.py +76 -49
  24. cmem_cmemc/config_parser.py +44 -0
  25. cmem_cmemc/context.py +168 -99
  26. cmem_cmemc/exceptions.py +15 -2
  27. cmem_cmemc/manual_helper/graph.py +2 -0
  28. cmem_cmemc/manual_helper/multi_page.py +1 -1
  29. cmem_cmemc/manual_helper/single_page.py +2 -0
  30. cmem_cmemc/migrations/remove_noop_triple_251.py +50 -0
  31. cmem_cmemc/migrations/shapes_widget_integrations_243.py +0 -5
  32. cmem_cmemc/object_list.py +3 -3
  33. cmem_cmemc/parameter_types/path.py +7 -0
  34. cmem_cmemc/placeholder.py +69 -0
  35. cmem_cmemc/utils.py +49 -15
  36. {cmem_cmemc-24.3.3.dist-info → cmem_cmemc-25.1.1.dist-info}/METADATA +14 -14
  37. cmem_cmemc-25.1.1.dist-info/RECORD +59 -0
  38. cmem_cmemc-25.1.1.dist-info/entry_points.txt +3 -0
  39. cmem_cmemc-24.3.3.dist-info/RECORD +0 -54
  40. cmem_cmemc-24.3.3.dist-info/entry_points.txt +0 -3
  41. {cmem_cmemc-24.3.3.dist-info → cmem_cmemc-25.1.1.dist-info}/LICENSE +0 -0
  42. {cmem_cmemc-24.3.3.dist-info → cmem_cmemc-25.1.1.dist-info}/WHEEL +0 -0
cmem_cmemc/context.py CHANGED
@@ -24,12 +24,14 @@ from rich.console import Console
24
24
  from rich.table import Table
25
25
  from urllib3.exceptions import InsecureRequestWarning
26
26
 
27
+ from cmem_cmemc.config_parser import PureSectionConfigParser
27
28
  from cmem_cmemc.exceptions import InvalidConfigurationError
28
29
  from cmem_cmemc.string_processor import StringProcessor, process_row
30
+ from cmem_cmemc.utils import is_enabled, str_to_bool
29
31
 
30
- DI_TARGET_VERSION = "v24.3.0"
32
+ DI_TARGET_VERSION = "v25.1.0"
31
33
 
32
- EXPLORE_TARGET_VERSION = "v24.3.0"
34
+ EXPLORE_TARGET_VERSION = "v25.1.0"
33
35
 
34
36
  KNOWN_CONFIG_KEYS = {
35
37
  "CMEM_BASE_URI": cmempy_config.get_cmem_base_uri,
@@ -56,34 +58,77 @@ SSL_VERIFY_WARNING = "SSL verification is disabled (SSL_VERIFY=False)."
56
58
  class ApplicationContext:
57
59
  """Context of the command line interface."""
58
60
 
61
+ APP_NAME: str = "cmemc"
62
+ DEFAULT_CONFIG_FILE: str = str(Path(click.get_app_dir(APP_NAME)) / "config.ini")
63
+ DEFAULT_EXTERNAL_HTTP_TIMEOUT: int = 10
64
+
59
65
  debug: bool
60
66
  quiet: bool
67
+ external_http_timeout: int
61
68
  config_dir: Path
62
69
  config_file: Path
63
- config: configparser.RawConfigParser
64
- connection: configparser.SectionProxy | None
70
+ config: PureSectionConfigParser
71
+ connection: str | None
65
72
  console: Console
66
73
  console_width: int | None = None
67
74
 
68
75
  # pylint: disable=too-many-instance-attributes
69
76
  # pylint: disable=too-many-public-methods
70
77
 
71
- def __init__(self, debug: bool = False, quiet: bool = False):
78
+ def __init__(
79
+ self,
80
+ config_file: str,
81
+ connection: str | None = None,
82
+ debug: bool = False,
83
+ quiet: bool = False,
84
+ ):
72
85
  """Initialize main context."""
86
+ self.config_file = Path(config_file)
73
87
  self.app_name = "cmemc"
74
88
  self.set_debug(debug)
75
89
  self.set_quiet(quiet)
76
- self.set_config_dir()
77
- self.set_config_file()
78
- if "CMEMC_CONFIG_FILE" in env:
79
- self.config_file_default = env["CMEMC_CONFIG_FILE"]
80
- else:
81
- self.config_file_default = str(self.config_dir / "config.ini")
82
- # since CMEM-3199, we do not initialize the connection on init
83
- self.connection = None
90
+ self.ensure_app_config_dir()
91
+ self.set_connection(connection)
92
+ self.set_external_http_timeout(self.DEFAULT_EXTERNAL_HTTP_TIMEOUT)
84
93
  self.console = Console(markup=True, emoji_variant="emoji")
85
94
  self.update_console_width()
86
95
 
96
+ @staticmethod
97
+ def from_params(params: dict) -> "ApplicationContext":
98
+ """Create an ApplicationContext instance from a dictionary.
99
+
100
+ Expects 'config_file' key to be present in the dictionary.
101
+ """
102
+ config_file = params.get("config_file")
103
+ debug = str_to_bool(str(params.get("debug")))
104
+ quiet = str_to_bool(str(params.get("quiet")))
105
+ external_http_timeout = (
106
+ int(str(params.get("external_http_timeout")))
107
+ if "external_http_timeout" in params
108
+ else ApplicationContext.DEFAULT_EXTERNAL_HTTP_TIMEOUT
109
+ )
110
+ connection = str(params.get("connection")) if params.get("connection") else None
111
+ if not config_file:
112
+ raise ValueError("Missing required key: 'config_file' in config dictionary")
113
+ app = ApplicationContext(
114
+ config_file=config_file, connection=connection, debug=debug, quiet=quiet
115
+ )
116
+ app.set_external_http_timeout(external_http_timeout)
117
+ if not app.debug:
118
+ app.set_debug(is_enabled(params, app.get_config(), "debug"))
119
+ if not app.quiet:
120
+ app.set_quiet(is_enabled(params, app.get_config(), "quiet"))
121
+ app.echo_debug(f"use config file: {app.config_file}")
122
+ if app.connection:
123
+ app.echo_debug(f"set connection: {app.connection}")
124
+ defaults = app.get_defaults()
125
+ app.apply_config(defaults)
126
+ section_config = app.get_connection_config()
127
+ app.clear_known_keys()
128
+ app.apply_config(section_config)
129
+ app.configure_cmempy()
130
+ return app
131
+
87
132
  def update_console_width(self) -> None:
88
133
  """Update console width from environment variable."""
89
134
  console_width_env = getenv("CMEMC_CONSOLE_WIDTH", None)
@@ -98,7 +143,7 @@ class ApplicationContext:
98
143
  today = str(datetime.now(tz=timezone.utc).date())
99
144
  data.update(date=today)
100
145
  if self.connection is not None:
101
- data.update(connection=self.connection.name)
146
+ data.update(connection=self.connection)
102
147
  else:
103
148
  data.update(connection="unnamed")
104
149
  return data
@@ -111,8 +156,13 @@ class ApplicationContext:
111
156
  """Set quiets state"""
112
157
  self.quiet = quiet
113
158
 
114
- def set_config_dir(self) -> None:
115
- """Set the configuration directory"""
159
+ def set_external_http_timeout(self, timeout: int) -> None:
160
+ """Set external http timeout"""
161
+ self.external_http_timeout = timeout
162
+ os.environ["CMEMC_EXTERNAL_HTTP_TIMEOUT"] = str(timeout)
163
+
164
+ def ensure_app_config_dir(self) -> None:
165
+ """Ensure the application's configuration directory exists."""
116
166
  try:
117
167
  self.config_dir = Path(click.get_app_dir(self.app_name))
118
168
  self.config_dir.mkdir(parents=True, exist_ok=True)
@@ -127,84 +177,77 @@ class ApplicationContext:
127
177
  self.config_file = self.config_dir / "config.ini"
128
178
  self.echo_debug(f"Set config to {self.config_file.absolute()}")
129
179
 
130
- def configure_cmempy(self, config: configparser.SectionProxy | None = None) -> None:
131
- """Configure the cmempy API to use a new connection."""
132
- # With or without connection config, we do not want API stdout prints
133
- env["CMEMPY_IS_CHATTY"] = "False"
134
- if config is None:
135
- return
136
- for key in KNOWN_CONFIG_KEYS:
137
- # with a loaded config, we delete all known keys from outside
138
- if key in env:
139
- env.pop(key)
140
- if key in config:
141
- value = str(config[key.lower()])
142
- env[key] = value
180
+ def get_defaults(self) -> dict[str, str]:
181
+ """Populate missing values in `params` using defaults from the configuration."""
182
+ config = self.get_config()
183
+ defaults = dict(config.defaults())
184
+ if "CMEMC_CONFIG_FILE" in defaults:
185
+ defaults.pop("CMEMC_CONFIG_FILE")
186
+ if "CMEMC_DEBUG" in defaults and self.debug:
187
+ defaults.pop("CMEMC_DEBUG")
188
+ if "CMEMC_QUIET" in defaults and self.quiet:
189
+ defaults.pop("CMEMC_QUIET")
190
+ if "CMEMC_CONNECTION" in defaults and self.connection:
191
+ defaults.pop("CMEMC_CONNECTION")
192
+ if (
193
+ "CMEMC_EXTERNAL_HTTP_TIMEOUT" in defaults
194
+ and self.external_http_timeout != self.DEFAULT_EXTERNAL_HTTP_TIMEOUT
195
+ ):
196
+ defaults.pop("CMEMC_EXTERNAL_HTTP_TIMEOUT")
197
+ for key in defaults:
198
+ if key in os.environ:
199
+ defaults.pop(key)
200
+ return defaults
201
+
202
+ def get_connection_config(self) -> dict[str, str]:
203
+ """Get connection section config"""
204
+ if not self.connection:
205
+ return {}
206
+ config = dict(self.get_config()[self.connection])
207
+ if "CMEMC_CONFIG_FILE" in config:
208
+ config.pop("CMEMC_CONFIG_FILE")
209
+ if "CMEMC_CONNECTION" in config:
210
+ config.pop("CMEMC_CONNECTION")
211
+ return config
212
+
213
+ def apply_config(self, config: dict[str, str]) -> None:
214
+ """Apply the configuration to the context."""
215
+ for key, value in config.items():
216
+ if key == "CMEMC_DEBUG":
217
+ self.set_debug(str_to_bool(value))
218
+ elif key == "CMEMC_QUIET":
219
+ self.set_quiet(str_to_bool(value))
220
+ elif key == "CMEMC_CONNECTION":
221
+ self.echo_debug(f"set connection to {value}")
222
+ self.set_connection(value)
223
+ elif key == "CMEMC_EXTERNAL_HTTP_TIMEOUT":
224
+ self.set_external_http_timeout(int(value))
225
+ else:
143
226
  if key in KNOWN_SECRET_KEYS:
144
227
  self.echo_debug(key + " set by config")
145
228
  else:
146
229
  self.echo_debug(key + " set by config to " + value)
230
+ env[key] = value
147
231
 
148
- # allow to fetch all secrets from an external process
149
- for _ in KNOWN_SECRET_KEYS:
150
- self.set_credential_from_process(_, _ + "_PROCESS", config)
151
-
152
- self.echo_debug(f"CA bundle loaded from {cmempy_config.get_requests_ca_bundle()}")
153
- return
232
+ def clear_known_keys(self) -> None:
233
+ """Clear know keys if the connection is set."""
234
+ if self.connection:
235
+ # clear all known keys
236
+ for key in KNOWN_CONFIG_KEYS:
237
+ if key in env:
238
+ env.pop(key)
154
239
 
155
- def set_connection_from_params(self, params: dict) -> None:
156
- """Set connection and config by manually checking params (completion)."""
157
- self.set_config_file(getenv("CMEMC_CONFIG_FILE", str(self.config_file)))
158
- if params["config_file"]:
159
- self.set_config_file(params["config_file"])
160
-
161
- self.config = self.get_config()
162
- # look for connection in environment and set connection
163
- self.set_connection(getenv("CMEMC_CONNECTION", ""))
164
- if params["connection"]:
165
- self.set_connection(params["connection"])
166
-
167
- def set_connection_from_args(self, args: dict) -> None:
168
- """Set connection and config by manually checking param (completion)."""
169
- # look for environment and load config
170
- self.set_config_file(getenv("CMEMC_CONFIG_FILE", str(self.config_file)))
171
- # look for config file in arguments and load config
172
- found_config_file = False
173
- for arg in args:
174
- if found_config_file is True:
175
- self.set_config_file(arg)
176
- break
177
- if arg == "--config-file":
178
- found_config_file = True
179
- self.config = self.get_config()
180
- # look for connection in environment and set connection
181
- self.set_connection(getenv("CMEMC_CONNECTION", ""))
182
- # look for connection in arguments and set connection
183
- found_connection = False
184
- for arg in args:
185
- if found_connection is True:
186
- self.set_connection(arg)
187
- return
188
- if arg in ("-c", "--connection"):
189
- found_connection = True
190
- return
191
-
192
- def set_connection(self, section_string: str) -> configparser.SectionProxy | None:
193
- """Set connection config section based on section string."""
194
- self.config = self.get_config()
195
- self.connection = None
196
- if section_string is None or section_string == "":
197
- self.echo_debug("No config given, use API defaults or environment connection.")
198
- elif section_string not in self.config:
199
- raise InvalidConfigurationError(
200
- f"There is no connection '{section_string}' configured in "
201
- f"config '{self.config_file}'."
202
- )
203
- else:
204
- self.echo_debug(f"Use connection config: {section_string}")
205
- self.connection = self.config[section_string]
206
- self.configure_cmempy(self.connection)
240
+ def configure_cmempy(self) -> None:
241
+ """Configure the cmempy API to use a new connection."""
242
+ # With or without connection config, we do not want API stdout prints
243
+ env["CMEMPY_IS_CHATTY"] = "False"
244
+ if self.connection:
245
+ config = self.get_config()[self.connection]
246
+ # allow to fetch all secrets from an external process
247
+ for _ in KNOWN_SECRET_KEYS:
248
+ self.set_credential_from_process(_, _ + "_PROCESS", config)
207
249
 
250
+ self.echo_debug(f"CA bundle loaded from {cmempy_config.get_requests_ca_bundle()}")
208
251
  # If cert validation is disabled, output a warning
209
252
  # Also disable library warnings:
210
253
  # https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
@@ -212,27 +255,49 @@ class ApplicationContext:
212
255
  self.echo_warning(SSL_VERIFY_WARNING)
213
256
  urllib3.disable_warnings(category=InsecureRequestWarning)
214
257
 
215
- return self.connection
258
+ @staticmethod
259
+ def set_connection_from_params(params: dict) -> None:
260
+ """Set connection and config by manually checking params (completion)."""
261
+ app = ApplicationContext.from_params(params)
262
+ defaults = app.get_defaults()
263
+ app.apply_config(defaults)
264
+ section_config = app.get_connection_config()
265
+ app.clear_known_keys()
266
+ app.apply_config(section_config)
267
+ app.configure_cmempy()
268
+
269
+ def set_connection(self, section_string: str | None = None) -> None:
270
+ """Set connection config section based on section string."""
271
+ self.connection = None
272
+ if section_string is None or section_string == "":
273
+ return
274
+ if section_string not in self.get_config():
275
+ raise InvalidConfigurationError(
276
+ self,
277
+ f"There is no connection '{section_string}' configured in "
278
+ f"config '{self.config_file}'.",
279
+ )
280
+ self.connection = section_string
216
281
 
217
282
  def get_config_file(self) -> Path:
218
283
  """Check the connection config file."""
219
284
  if not self.config_file.exists():
220
285
  with self.config_file.open(mode="a", encoding="UTF-8"):
221
286
  self.echo_warning(f"Empty config created: {self.config_file.name}")
222
- self.echo_debug("Config loaded: " + self.config_file.name)
223
287
  return self.config_file
224
288
 
225
- def get_config(self) -> configparser.RawConfigParser:
289
+ def get_config(self) -> PureSectionConfigParser:
226
290
  """Parse the configuration"""
227
- config = configparser.RawConfigParser()
291
+ config = PureSectionConfigParser()
228
292
  try:
229
293
  # https://stackoverflow.com/questions/1648517/
230
294
  config.read(self.get_config_file(), encoding="utf-8")
231
295
  except configparser.Error as error:
232
296
  raise InvalidConfigurationError(
297
+ self,
233
298
  "The following config parser error needs to be fixed with your config file:\n"
234
299
  f"{error!s}\n"
235
- "You can use the 'config edit' command to fix this."
300
+ "You can use the 'config edit' command to fix this.",
236
301
  ) from error
237
302
  except Exception as error: # noqa: BLE001
238
303
  self.echo_debug(f"Could not read config file - provide empty config: {error!s}")
@@ -282,12 +347,19 @@ class ApplicationContext:
282
347
  """Output a debug message if --debug is enabled.
283
348
 
284
349
  2024-05-17: also allows list of strings now
350
+ 2025-04-07: extract output function
285
351
  """
286
352
  messages: list[str] = [message] if isinstance(message, str) else message
287
- if self.debug:
288
- now = datetime.now(tz=timezone.utc)
289
- for _ in messages:
290
- click.secho(f"[{now!s}] {_}", err=True, dim=True)
353
+ if self.debug and not ApplicationContext.is_completing():
354
+ self.echo_debug_string(messages)
355
+
356
+ @staticmethod
357
+ def echo_debug_string(message: str | list[str]) -> None:
358
+ """Output a debug message"""
359
+ messages: list[str] = [message] if isinstance(message, str) else message
360
+ now = datetime.now(tz=timezone.utc)
361
+ for _ in messages:
362
+ click.secho(f"[{now!s}] {_}", err=True, dim=True)
291
363
 
292
364
  def echo_info(
293
365
  self,
@@ -491,6 +563,3 @@ class ApplicationContext:
491
563
  pw_value = split_output[0]
492
564
  # set the password environment variable
493
565
  env[pw_key] = pw_value
494
-
495
-
496
- CONTEXT = ApplicationContext()
cmem_cmemc/exceptions.py CHANGED
@@ -1,9 +1,22 @@
1
1
  """Declares all cli exceptions."""
2
2
 
3
+ from typing import TYPE_CHECKING
3
4
 
4
- class InvalidConfigurationError(ValueError):
5
+ if TYPE_CHECKING:
6
+ from cmem_cmemc.context import ApplicationContext
7
+
8
+
9
+ class CmemcError(ValueError):
10
+ """Base exception for CMEM-CMEMC-related errors."""
11
+
12
+ def __init__(self, app: "ApplicationContext", *args: str):
13
+ super().__init__(*args)
14
+ self.app = app
15
+
16
+
17
+ class InvalidConfigurationError(CmemcError):
5
18
  """The configuration given was not found or is broken."""
6
19
 
7
20
 
8
- class ServerError(ValueError):
21
+ class ServerError(CmemcError):
9
22
  """The server reported an error with a process."""
@@ -43,6 +43,8 @@ def print_group_manual_graph_recursive(
43
43
  """Output documentation graph (recursive)."""
44
44
  commands = command_group.commands # type: ignore[union-attr]
45
45
  for key in commands:
46
+ if key == "manual":
47
+ continue
46
48
  item = commands[key]
47
49
  iri = f":{prefix}{key}"
48
50
  ctx.obj.echo_info(f"{iri} skos:notation '{key}' .")
@@ -205,7 +205,7 @@ tags:
205
205
 
206
206
 
207
207
  def create_multi_page_documentation(ctx: click.core.Context, directory: str) -> None:
208
- """Create a multi page reference manual for documentation.eccenca.com.
208
+ """Create a multipage reference manual for documentation.eccenca.com.
209
209
 
210
210
  Returns: None
211
211
  """
@@ -24,6 +24,8 @@ def print_group_manual_recursive(
24
24
  """Output the help text of a command group (recursive)."""
25
25
  commands = command_group.commands # type: ignore[union-attr]
26
26
  for key in commands:
27
+ if key == "manual":
28
+ continue
27
29
  command = commands[key]
28
30
  formatter = ctx.make_formatter()
29
31
  if isinstance(command, click.Group):
@@ -0,0 +1,50 @@
1
+ """RemoveHideHeaderFooterStatements migration"""
2
+
3
+ from typing import ClassVar
4
+
5
+ from cmem_cmemc.migrations.abc import MigrationRecipe, components
6
+
7
+
8
+ class RemoveHideHeaderFooterStatements(MigrationRecipe):
9
+ """25.1 Remove Non-Operational shui:valueQueryHideHeader|Footer Triple"""
10
+
11
+ id = "hide-header-footer-25.1"
12
+ description = "Remove triples using deprecated shui:valueQueryHideHeader|Footer terms"
13
+ component: components = "explore"
14
+ first_version = "25.1"
15
+ tags: ClassVar[list[str]] = ["shapes", "user"]
16
+ check_query = """{{DEFAULT_PREFIXES}}
17
+ SELECT ?shape
18
+ WHERE {
19
+ GRAPH ?shapeCatalog {
20
+ ?shapeCatalog a shui:ShapeCatalog .
21
+ ?shape ?deprecatedProperty ?value .
22
+ }
23
+ VALUES ?deprecatedProperty {
24
+ shui:valueQueryHideHeader shui:valueQueryHideFooter
25
+ }
26
+ } LIMIT 1
27
+ """
28
+ delete_query = """{{DEFAULT_PREFIXES}}
29
+ DELETE {
30
+ GRAPH ?shapeCatalog { ?shape ?deprecatedProperty ?value . }
31
+ }
32
+ WHERE {
33
+ GRAPH ?shapeCatalog {
34
+ ?shapeCatalog a shui:ShapeCatalog .
35
+ ?shape ?deprecatedProperty ?value .
36
+ }
37
+ VALUES ?deprecatedProperty {
38
+ shui:valueQueryHideHeader shui:valueQueryHideFooter
39
+ }
40
+ }
41
+ """
42
+
43
+ def is_applicable(self) -> bool:
44
+ """Test if the recipe can be applied."""
45
+ existing_triples = self._select(self.check_query)
46
+ return len(existing_triples) == 1
47
+
48
+ def apply(self) -> None:
49
+ """Apply the recipe to the current version."""
50
+ self._update(self.delete_query)
@@ -27,11 +27,6 @@ WHERE {
27
27
  }
28
28
  """
29
29
  move_query = """{{DEFAULT_PREFIXES}}
30
- PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
31
- PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
32
- PREFIX shui: <https://vocab.eccenca.com/shui/>
33
- PREFIX sh: <http://www.w3.org/ns/shacl#>
34
-
35
30
  DELETE {
36
31
  GRAPH ?shapeCatalog {
37
32
  ?nodeShape shui:provideChartVisualization ?shuiChart .
cmem_cmemc/object_list.py CHANGED
@@ -10,7 +10,7 @@ from click import Argument, Context, UsageError
10
10
  from click.shell_completion import CompletionItem
11
11
 
12
12
  from cmem_cmemc.completion import finalize_completion, get_completion_args
13
- from cmem_cmemc.context import CONTEXT
13
+ from cmem_cmemc.context import ApplicationContext
14
14
  from cmem_cmemc.title_helper import TitleHelper
15
15
 
16
16
 
@@ -291,7 +291,7 @@ class ObjectList:
291
291
  if not isinstance(added_filter, Filter):
292
292
  raise TypeError("'filter_' parameter must be an instance OR a subclass of Filter")
293
293
  if added_filter.name in self.filters:
294
- raise ValueError(f"Filter {added_filter.name} already exists")
294
+ raise UsageError(f"Filter {added_filter.name} already exists")
295
295
  self.filters[added_filter.name] = added_filter
296
296
 
297
297
  def remove_filter(self, filter_name: str) -> None:
@@ -361,7 +361,7 @@ class ObjectList:
361
361
  incomplete=incomplete,
362
362
  )
363
363
 
364
- CONTEXT.set_connection_from_params(ctx.find_root().params)
364
+ ApplicationContext.set_connection_from_params(ctx.find_root().params)
365
365
  # This will filter the object list with name/values filter from the command line,
366
366
  # up to the current filter which values are completed
367
367
  objects = self.apply_filters(ctx=ctx, filter_=previous_filter)
@@ -8,6 +8,8 @@ import smart_open
8
8
  from click.core import Context, Parameter
9
9
  from smart_open import compression
10
10
 
11
+ from cmem_cmemc.context import ApplicationContext
12
+
11
13
 
12
14
  class ClickSmartPath(click.Path):
13
15
  """Custom Click smart_path ParamType"""
@@ -61,6 +63,11 @@ class ClickSmartPath(click.Path):
61
63
  """Open the file and return the file handle."""
62
64
  if file_path == "-":
63
65
  return click.open_file(file_path, mode=mode)
66
+ if transport_params is None:
67
+ transport_params = {}
68
+ transport_params["timeout"] = os.getenv(
69
+ "CMEM_TRANSPORT_TIMEOUT", ApplicationContext.DEFAULT_EXTERNAL_HTTP_TIMEOUT
70
+ )
64
71
  return smart_open.open( # type: ignore[no-any-return]
65
72
  file_path,
66
73
  mode,
@@ -0,0 +1,69 @@
1
+ """Class to retrieve query placeholder resources
2
+
3
+ note: this is not the correct place for this class - it should be cmem_client or cmempy
4
+ """
5
+
6
+ from cmem.cmempy.queries import SparqlQuery
7
+
8
+ FETCH_PLACEHOLDERS = """
9
+ PREFIX shui: <https://vocab.eccenca.com/shui/>
10
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
11
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
12
+
13
+ SELECT ?key ?valueQuery
14
+ FROM <{{graph}}>
15
+ WHERE {
16
+ ?placeholder a shui:QueryPlaceholder ;
17
+ shui:QueryPlaceholder_key ?key ;
18
+ shui:QueryPlaceholder_valueQuery ?valueQueryR ;
19
+ shui:QueryPlaceholder_usedInQuery <{{query}}> .
20
+ ?valueQueryR shui:queryText ?valueQuery
21
+ }
22
+ """
23
+
24
+
25
+ class QueryPlaceholder:
26
+ """A query placeholder"""
27
+
28
+ key: str
29
+ value_query: SparqlQuery
30
+
31
+ def __init__(self, key: str, value_query: str):
32
+ self.key = key
33
+ self.value_query = SparqlQuery(value_query, query_type="SELECT")
34
+
35
+ def complete(self, incomplete: str = "") -> list[str] | list[tuple[str, str]]: # noqa: ARG002
36
+ """Prepare a list of placeholder values"""
37
+ result = self.value_query.get_json_results()
38
+ projection_vars = result["head"]["vars"]
39
+ bindings = result["results"]["bindings"]
40
+ if "value" not in projection_vars:
41
+ return []
42
+
43
+ if "description" not in projection_vars:
44
+ values_without_description = []
45
+ for _ in bindings:
46
+ value = str(_["value"]["value"])
47
+ values_without_description.append(value)
48
+ return values_without_description
49
+
50
+ values_with_description = []
51
+ for _ in bindings:
52
+ value = str(_["value"]["value"])
53
+ description = str(_["description"]["value"])
54
+ values_with_description.append((value, description))
55
+ return values_with_description
56
+
57
+
58
+ def get_placeholders_for_query(iri: str) -> dict[str, QueryPlaceholder]:
59
+ """Get a placeholder dict for a query"""
60
+ placeholders = {}
61
+ results = SparqlQuery(
62
+ FETCH_PLACEHOLDERS,
63
+ query_type="SELECT",
64
+ ).get_json_results(placeholder={"graph": "https://ns.eccenca.com/data/queries/", "query": iri})
65
+ for binding in results["results"]["bindings"]:
66
+ key = binding["key"]["value"]
67
+ value_query = binding["valueQuery"]["value"]
68
+ placeholders[key] = QueryPlaceholder(key=key, value_query=value_query)
69
+ return placeholders