sparql-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,886 @@
1
+ """Convenience commands for common SPARQL exploration tasks (Phase 5)."""
2
+
3
+ import sys
4
+ import time
5
+
6
+ import typer
7
+
8
+ from sparql._version import __version__
9
+ from sparql.cli.output import OutputFormat
10
+ from sparql.core.client import SPARQLClient
11
+ from sparql.core.config import AuthType, load_config, resolve_config
12
+ from sparql.core.exceptions import ConfigError, NetworkError
13
+ from sparql.core.exceptions import TimeoutError as SPARQLTimeoutError
14
+ from sparql.core.exit_codes import ExitCode
15
+ from sparql.core.logging import get_logger
16
+ from sparql.core.prefixes import PrefixResolver
17
+ from sparql.formatters import (
18
+ CSVFormatter,
19
+ Formatter,
20
+ JSONFormatter,
21
+ TableFormatter,
22
+ TSVFormatter,
23
+ )
24
+
25
+
26
+ def _detect_default_format() -> OutputFormat:
27
+ if sys.stdout.isatty():
28
+ return OutputFormat.table
29
+ return OutputFormat.tsv
30
+
31
+
32
+ def _get_formatter(
33
+ output_format: OutputFormat,
34
+ prefix_resolver: PrefixResolver | None = None,
35
+ ) -> Formatter:
36
+ if output_format == OutputFormat.json:
37
+ return JSONFormatter(jsonl=False, prefix_resolver=prefix_resolver)
38
+ if output_format == OutputFormat.jsonl:
39
+ return JSONFormatter(jsonl=True, prefix_resolver=prefix_resolver)
40
+ if output_format == OutputFormat.table:
41
+ return TableFormatter(max_width=60, prefix_resolver=prefix_resolver)
42
+ if output_format == OutputFormat.csv:
43
+ return CSVFormatter(skip_header=False, prefix_resolver=prefix_resolver)
44
+ if output_format == OutputFormat.tsv:
45
+ return TSVFormatter(skip_header=False, prefix_resolver=prefix_resolver)
46
+ # Default to JSON
47
+ return JSONFormatter(jsonl=False, prefix_resolver=prefix_resolver)
48
+
49
+
50
+ def _get_global_options(
51
+ ctx: typer.Context | None,
52
+ ) -> tuple[str | None, str | None, bool, str | None]:
53
+ if ctx and ctx.obj:
54
+ return (
55
+ ctx.obj.get("profile"),
56
+ ctx.obj.get("endpoint"),
57
+ ctx.obj.get("show_graphs", False),
58
+ ctx.obj.get("graph_filter"),
59
+ )
60
+ return None, None, False, None
61
+
62
+
63
+ def _execute_convenience_query(
64
+ query: str,
65
+ endpoint: str | None,
66
+ profile: str | None,
67
+ timeout: float | None,
68
+ format: OutputFormat | None,
69
+ verbose: bool = False,
70
+ ctx: typer.Context | None = None,
71
+ ) -> None:
72
+ """Execute a SPARQL query and output formatted results.
73
+
74
+ Handles configuration resolution, client creation, execution, and error handling.
75
+ """
76
+ logger = get_logger("convenience")
77
+
78
+ # Merge global options (command-specific takes precedence)
79
+ global_profile, global_endpoint, _, _ = _get_global_options(ctx)
80
+ profile = profile or global_profile
81
+ endpoint = endpoint or global_endpoint
82
+
83
+ try:
84
+ config = load_config()
85
+ resolved = resolve_config(
86
+ config,
87
+ profile=profile,
88
+ cli_endpoint=endpoint,
89
+ cli_timeout=timeout,
90
+ )
91
+ except ConfigError as e:
92
+ typer.echo(f"Config error: {e}", err=True)
93
+ raise typer.Exit(ExitCode.CONFIG_ERROR) from e
94
+
95
+ if verbose:
96
+ typer.echo(f"Endpoint: {resolved.endpoint}", err=True)
97
+ typer.echo(f"Timeout: {resolved.timeout}s", err=True)
98
+ typer.echo("Query:", err=True)
99
+ typer.echo(query, err=True)
100
+ typer.echo("---", err=True)
101
+
102
+ logger.debug(
103
+ "endpoint.resolved",
104
+ endpoint=resolved.endpoint,
105
+ timeout=resolved.timeout,
106
+ )
107
+
108
+ client = SPARQLClient(
109
+ endpoint_url=resolved.endpoint,
110
+ timeout=resolved.timeout,
111
+ user_agent=resolved.user_agent or f"sparql-cli/{__version__}",
112
+ username=resolved.username,
113
+ password=resolved.password,
114
+ digest_auth=resolved.auth_type == AuthType.DIGEST,
115
+ )
116
+
117
+ output_format = format if format is not None else _detect_default_format()
118
+
119
+ # Create prefix resolver if prefixes configured
120
+ prefix_resolver = PrefixResolver(config.prefixes) if config.prefixes else None
121
+ formatter = _get_formatter(output_format, prefix_resolver)
122
+
123
+ logger.debug("query.execute", query=query, query_bytes=len(query))
124
+
125
+ try:
126
+ start_time = time.perf_counter()
127
+ results = client.execute(query)
128
+ result_count = 0
129
+ for line in formatter.format(results):
130
+ typer.echo(line)
131
+ result_count += 1
132
+ elapsed = time.perf_counter() - start_time
133
+ logger.debug(
134
+ "query.complete", duration_s=round(elapsed, 3), results=result_count
135
+ )
136
+ except SPARQLTimeoutError as e:
137
+ typer.echo(f"Timeout: {e}", err=True)
138
+ raise typer.Exit(ExitCode.TIMEOUT) from e
139
+ except NetworkError as e:
140
+ typer.echo(f"Error: {e}", err=True)
141
+ raise typer.Exit(ExitCode.NETWORK_ERROR) from e
142
+
143
+
144
+ def graphs(
145
+ ctx: typer.Context,
146
+ endpoint: str | None = typer.Option( # noqa: B008
147
+ None,
148
+ "--endpoint",
149
+ "-E",
150
+ help="SPARQL endpoint URL (overrides config)",
151
+ ),
152
+ profile: str | None = typer.Option( # noqa: B008
153
+ None,
154
+ "--profile",
155
+ "-P",
156
+ help="Use named endpoint profile from config",
157
+ ),
158
+ limit: int = typer.Option( # noqa: B008
159
+ 100,
160
+ "--limit",
161
+ "-n",
162
+ help="Maximum number of results (default: 100)",
163
+ ),
164
+ timeout: float | None = typer.Option( # noqa: B008
165
+ None,
166
+ "--timeout",
167
+ "-t",
168
+ help="Query timeout in seconds",
169
+ ),
170
+ format: OutputFormat | None = typer.Option( # noqa: B008
171
+ None,
172
+ "--format",
173
+ "-f",
174
+ help="Output format (default: table for TTY, tsv for pipes)",
175
+ ),
176
+ verbose: bool = typer.Option( # noqa: B008
177
+ False,
178
+ "--verbose",
179
+ "-v",
180
+ help="Show endpoint and query before execution",
181
+ ),
182
+ ) -> None:
183
+ """List named graphs in the endpoint.
184
+
185
+ Shows all distinct named graphs that contain data.
186
+ """
187
+ query = (
188
+ "SELECT DISTINCT ?graph WHERE { GRAPH ?graph { ?s ?p ?o } } "
189
+ f"LIMIT {limit}"
190
+ )
191
+ _execute_convenience_query(query, endpoint, profile, timeout, format, verbose, ctx)
192
+
193
+
194
+ def classes(
195
+ ctx: typer.Context,
196
+ endpoint: str | None = typer.Option( # noqa: B008
197
+ None,
198
+ "--endpoint",
199
+ "-E",
200
+ help="SPARQL endpoint URL (overrides config)",
201
+ ),
202
+ profile: str | None = typer.Option( # noqa: B008
203
+ None,
204
+ "--profile",
205
+ "-P",
206
+ help="Use named endpoint profile from config",
207
+ ),
208
+ limit: int = typer.Option( # noqa: B008
209
+ 100,
210
+ "--limit",
211
+ "-n",
212
+ help="Maximum number of results (default: 100)",
213
+ ),
214
+ timeout: float | None = typer.Option( # noqa: B008
215
+ None,
216
+ "--timeout",
217
+ "-t",
218
+ help="Query timeout in seconds",
219
+ ),
220
+ format: OutputFormat | None = typer.Option( # noqa: B008
221
+ None,
222
+ "--format",
223
+ "-f",
224
+ help="Output format (default: table for TTY, tsv for pipes)",
225
+ ),
226
+ labels: bool = typer.Option( # noqa: B008
227
+ False,
228
+ "--labels/--no-labels",
229
+ "-l",
230
+ help="Include rdfs:label for each class (slower, not all endpoints)",
231
+ ),
232
+ show_graphs: bool = typer.Option( # noqa: B008
233
+ False,
234
+ "--graphs/--no-graphs",
235
+ "-g",
236
+ help="Show graph column in output",
237
+ ),
238
+ graph_filter: str | None = typer.Option( # noqa: B008
239
+ None,
240
+ "--graph",
241
+ "-G",
242
+ help="Filter to specific named graph URI",
243
+ ),
244
+ verbose: bool = typer.Option( # noqa: B008
245
+ False,
246
+ "--verbose",
247
+ "-v",
248
+ help="Show endpoint and query before execution",
249
+ ),
250
+ ) -> None:
251
+ """List distinct RDF classes from endpoint.
252
+
253
+ Use --labels to also fetch rdfs:label for each class (if available).
254
+ Use -g to show which graph each class comes from.
255
+ Use -G <uri> to query only that named graph.
256
+ """
257
+ # Merge global graph options (command-specific takes precedence)
258
+ _, _, global_show_graphs, global_graph_filter = _get_global_options(ctx)
259
+ show_graph = show_graphs or global_show_graphs
260
+ graph_uri = graph_filter or global_graph_filter
261
+
262
+ if labels:
263
+ # Use subquery: first find classes (limited), then lookup labels only for those
264
+ # Include PREFIX declaration for endpoints that require it (e.g., MarkLogic)
265
+ if show_graph:
266
+ query = f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
267
+ SELECT ?graph ?class ?label WHERE {{
268
+ {{
269
+ SELECT DISTINCT ?graph ?class
270
+ WHERE {{ GRAPH ?graph {{ ?s a ?class }} }} LIMIT {limit}
271
+ }}
272
+ OPTIONAL {{
273
+ ?class rdfs:label ?label .
274
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
275
+ }}
276
+ }}"""
277
+ elif graph_uri:
278
+ query = f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
279
+ SELECT ?class ?label WHERE {{
280
+ {{
281
+ SELECT DISTINCT ?class
282
+ WHERE {{ GRAPH <{graph_uri}> {{ ?s a ?class }} }} LIMIT {limit}
283
+ }}
284
+ OPTIONAL {{
285
+ ?class rdfs:label ?label .
286
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
287
+ }}
288
+ }}"""
289
+ else:
290
+ query = f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
291
+ SELECT ?class ?label WHERE {{
292
+ {{
293
+ SELECT DISTINCT ?class WHERE {{ ?s a ?class }} LIMIT {limit}
294
+ }}
295
+ OPTIONAL {{
296
+ ?class rdfs:label ?label .
297
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
298
+ }}
299
+ }}"""
300
+ else:
301
+ if show_graph:
302
+ query = (
303
+ "SELECT DISTINCT ?graph ?class "
304
+ f"WHERE {{ GRAPH ?graph {{ ?s a ?class }} }} LIMIT {limit}"
305
+ )
306
+ elif graph_uri:
307
+ query = (
308
+ "SELECT DISTINCT ?class "
309
+ f"WHERE {{ GRAPH <{graph_uri}> {{ ?s a ?class }} }} LIMIT {limit}"
310
+ )
311
+ else:
312
+ query = f"SELECT DISTINCT ?class WHERE {{ ?s a ?class }} LIMIT {limit}"
313
+ _execute_convenience_query(query, endpoint, profile, timeout, format, verbose, ctx)
314
+
315
+
316
+ def predicates(
317
+ ctx: typer.Context,
318
+ predicate_uri: str | None = typer.Argument( # noqa: B008
319
+ None,
320
+ help="Optional predicate URI to show values for (e.g., rdf:type)",
321
+ ),
322
+ endpoint: str | None = typer.Option( # noqa: B008
323
+ None,
324
+ "--endpoint",
325
+ "-E",
326
+ help="SPARQL endpoint URL (overrides config)",
327
+ ),
328
+ profile: str | None = typer.Option( # noqa: B008
329
+ None,
330
+ "--profile",
331
+ "-P",
332
+ help="Use named endpoint profile from config",
333
+ ),
334
+ limit: int = typer.Option( # noqa: B008
335
+ 100,
336
+ "--limit",
337
+ "-n",
338
+ help="Maximum number of results (default: 100)",
339
+ ),
340
+ timeout: float | None = typer.Option( # noqa: B008
341
+ None,
342
+ "--timeout",
343
+ "-t",
344
+ help="Query timeout in seconds",
345
+ ),
346
+ format: OutputFormat | None = typer.Option( # noqa: B008
347
+ None,
348
+ "--format",
349
+ "-f",
350
+ help="Output format (default: table for TTY, tsv for pipes)",
351
+ ),
352
+ labels: bool = typer.Option( # noqa: B008
353
+ False,
354
+ "--labels/--no-labels",
355
+ "-l",
356
+ help="Include rdfs:label (slower, not all endpoints)",
357
+ ),
358
+ values_only: bool = typer.Option( # noqa: B008
359
+ False,
360
+ "--values/--subjects",
361
+ help="Show only distinct values (objects), not subjects",
362
+ ),
363
+ show_graphs: bool = typer.Option( # noqa: B008
364
+ False,
365
+ "--graphs/--no-graphs",
366
+ "-g",
367
+ help="Show graph column in output",
368
+ ),
369
+ graph_filter: str | None = typer.Option( # noqa: B008
370
+ None,
371
+ "--graph",
372
+ "-G",
373
+ help="Filter to specific named graph URI",
374
+ ),
375
+ verbose: bool = typer.Option( # noqa: B008
376
+ False,
377
+ "--verbose",
378
+ "-v",
379
+ help="Show endpoint and query before execution",
380
+ ),
381
+ ) -> None:
382
+ """List predicates, or show usage of a specific predicate.
383
+
384
+ Without argument: lists all distinct predicates in the endpoint.
385
+ With PREDICATE_URI: shows subject-value pairs using that predicate.
386
+
387
+ Examples:
388
+ sparql predicates # list all predicates
389
+ sparql predicates rdf:type # show who has rdf:type and what values
390
+ sparql predicates rdf:type --values # show only distinct values
391
+ """
392
+ # Merge global graph options (command-specific takes precedence)
393
+ _, _, global_show_graphs, global_graph_filter = _get_global_options(ctx)
394
+ show_graph = show_graphs or global_show_graphs
395
+ graph_uri = graph_filter or global_graph_filter
396
+
397
+ # Expand prefixed predicate URI if provided
398
+ config = load_config()
399
+ if predicate_uri and config.prefixes:
400
+ resolver = PrefixResolver(config.prefixes)
401
+ predicate_uri = resolver.expand(predicate_uri)
402
+
403
+ if predicate_uri:
404
+ # Show usage of specific predicate
405
+ query = _build_predicate_usage_query(
406
+ predicate_uri, limit, labels, values_only, show_graph, graph_uri
407
+ )
408
+ else:
409
+ # List all predicates
410
+ query = _build_predicate_list_query(limit, labels, show_graph, graph_uri)
411
+
412
+ _execute_convenience_query(query, endpoint, profile, timeout, format, verbose, ctx)
413
+
414
+
415
+ def _build_predicate_list_query(
416
+ limit: int, labels: bool, show_graph: bool, graph_uri: str | None
417
+ ) -> str:
418
+ if labels:
419
+ if show_graph:
420
+ return f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
421
+ SELECT ?graph ?predicate ?label WHERE {{
422
+ {{
423
+ SELECT DISTINCT ?graph ?predicate
424
+ WHERE {{ GRAPH ?graph {{ ?s ?predicate ?o }} }} LIMIT {limit}
425
+ }}
426
+ OPTIONAL {{
427
+ ?predicate rdfs:label ?label .
428
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
429
+ }}
430
+ }}"""
431
+ elif graph_uri:
432
+ return f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
433
+ SELECT ?predicate ?label WHERE {{
434
+ {{
435
+ SELECT DISTINCT ?predicate
436
+ WHERE {{ GRAPH <{graph_uri}> {{ ?s ?predicate ?o }} }} LIMIT {limit}
437
+ }}
438
+ OPTIONAL {{
439
+ ?predicate rdfs:label ?label .
440
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
441
+ }}
442
+ }}"""
443
+ else:
444
+ return f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
445
+ SELECT ?predicate ?label WHERE {{
446
+ {{
447
+ SELECT DISTINCT ?predicate WHERE {{ ?s ?predicate ?o }} LIMIT {limit}
448
+ }}
449
+ OPTIONAL {{
450
+ ?predicate rdfs:label ?label .
451
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
452
+ }}
453
+ }}"""
454
+ else:
455
+ if show_graph:
456
+ return (
457
+ "SELECT DISTINCT ?graph ?predicate "
458
+ f"WHERE {{ GRAPH ?graph {{ ?s ?predicate ?o }} }} LIMIT {limit}"
459
+ )
460
+ elif graph_uri:
461
+ return (
462
+ "SELECT DISTINCT ?predicate "
463
+ f"WHERE {{ GRAPH <{graph_uri}> {{ ?s ?predicate ?o }} }} LIMIT {limit}"
464
+ )
465
+ else:
466
+ return (
467
+ "SELECT DISTINCT ?predicate "
468
+ f"WHERE {{ ?s ?predicate ?o }} LIMIT {limit}"
469
+ )
470
+
471
+
472
+ def _build_predicate_usage_query(
473
+ predicate_uri: str,
474
+ limit: int,
475
+ labels: bool,
476
+ values_only: bool,
477
+ show_graph: bool,
478
+ graph_uri: str | None,
479
+ ) -> str:
480
+ if values_only:
481
+ # Show only distinct values (objects)
482
+ if labels:
483
+ if show_graph:
484
+ return f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
485
+ SELECT ?graph ?value ?label WHERE {{
486
+ {{
487
+ SELECT DISTINCT ?graph ?value
488
+ WHERE {{ GRAPH ?graph {{ ?s <{predicate_uri}> ?value }} }} LIMIT {limit}
489
+ }}
490
+ OPTIONAL {{
491
+ ?value rdfs:label ?label .
492
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
493
+ }}
494
+ }}"""
495
+ elif graph_uri:
496
+ return f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
497
+ SELECT ?value ?label WHERE {{
498
+ {{
499
+ SELECT DISTINCT ?value
500
+ WHERE {{ GRAPH <{graph_uri}> {{ ?s <{predicate_uri}> ?value }} }} LIMIT {limit}
501
+ }}
502
+ OPTIONAL {{
503
+ ?value rdfs:label ?label .
504
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
505
+ }}
506
+ }}"""
507
+ else:
508
+ return f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
509
+ SELECT ?value ?label WHERE {{
510
+ {{
511
+ SELECT DISTINCT ?value
512
+ WHERE {{ ?s <{predicate_uri}> ?value }} LIMIT {limit}
513
+ }}
514
+ OPTIONAL {{
515
+ ?value rdfs:label ?label .
516
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
517
+ }}
518
+ }}"""
519
+ else:
520
+ if show_graph:
521
+ return (
522
+ "SELECT DISTINCT ?graph ?value "
523
+ f"WHERE {{ GRAPH ?graph {{ ?s <{predicate_uri}> ?value }} }} "
524
+ f"LIMIT {limit}"
525
+ )
526
+ elif graph_uri:
527
+ return (
528
+ "SELECT DISTINCT ?value "
529
+ f"WHERE {{ GRAPH <{graph_uri}> {{ ?s <{predicate_uri}> ?value }} }}"
530
+ f" LIMIT {limit}"
531
+ )
532
+ else:
533
+ return (
534
+ "SELECT DISTINCT ?value "
535
+ f"WHERE {{ ?s <{predicate_uri}> ?value }} LIMIT {limit}"
536
+ )
537
+ else:
538
+ # Show subject-value pairs
539
+ if show_graph:
540
+ return (
541
+ "SELECT DISTINCT ?graph ?subject ?value "
542
+ f"WHERE {{ GRAPH ?graph {{ ?subject <{predicate_uri}> ?value }} }} "
543
+ f"LIMIT {limit}"
544
+ )
545
+ elif graph_uri:
546
+ return (
547
+ "SELECT DISTINCT ?subject ?value WHERE "
548
+ f"{{ GRAPH <{graph_uri}> {{ ?subject <{predicate_uri}> ?value }} }}"
549
+ f" LIMIT {limit}"
550
+ )
551
+ else:
552
+ return (
553
+ "SELECT DISTINCT ?subject ?value "
554
+ f"WHERE {{ ?subject <{predicate_uri}> ?value }} LIMIT {limit}"
555
+ )
556
+
557
+
558
+ def explore(
559
+ ctx: typer.Context,
560
+ uri: str = typer.Argument( # noqa: B008
561
+ ...,
562
+ help="URI to explore (finds all triples where this URI appears)",
563
+ ),
564
+ endpoint: str | None = typer.Option( # noqa: B008
565
+ None,
566
+ "--endpoint",
567
+ "-E",
568
+ help="SPARQL endpoint URL (overrides config)",
569
+ ),
570
+ profile: str | None = typer.Option( # noqa: B008
571
+ None,
572
+ "--profile",
573
+ "-P",
574
+ help="Use named endpoint profile from config",
575
+ ),
576
+ limit: int = typer.Option( # noqa: B008
577
+ 100,
578
+ "--limit",
579
+ "-n",
580
+ help="Maximum number of results (default: 100)",
581
+ ),
582
+ timeout: float | None = typer.Option( # noqa: B008
583
+ None,
584
+ "--timeout",
585
+ "-t",
586
+ help="Query timeout in seconds",
587
+ ),
588
+ format: OutputFormat | None = typer.Option( # noqa: B008
589
+ None,
590
+ "--format",
591
+ "-f",
592
+ help="Output format (default: table for TTY, tsv for pipes)",
593
+ ),
594
+ show_graphs: bool = typer.Option( # noqa: B008
595
+ False,
596
+ "--graphs/--no-graphs",
597
+ "-g",
598
+ help="Show graph column in output",
599
+ ),
600
+ graph_filter: str | None = typer.Option( # noqa: B008
601
+ None,
602
+ "--graph",
603
+ "-G",
604
+ help="Filter to specific named graph URI",
605
+ ),
606
+ verbose: bool = typer.Option( # noqa: B008
607
+ False,
608
+ "--verbose",
609
+ "-v",
610
+ help="Show endpoint and query before execution",
611
+ ),
612
+ ) -> None:
613
+ """Explore triples where URI appears as subject, predicate, or object.
614
+
615
+ Finds all relationships involving the specified URI in any position.
616
+
617
+ URI can be a full URI or prefixed name (e.g., dbo:Person, wd:Q42).
618
+ Use -g to show which graph each triple comes from.
619
+ Use -G <uri> to query only that named graph.
620
+ """
621
+ # Merge global graph options (command-specific takes precedence)
622
+ _, _, global_show_graphs, global_graph_filter = _get_global_options(ctx)
623
+ show_graph = show_graphs or global_show_graphs
624
+ graph_uri = graph_filter or global_graph_filter
625
+
626
+ # Expand prefixed names to full URIs
627
+ config = load_config()
628
+ if config.prefixes:
629
+ resolver = PrefixResolver(config.prefixes)
630
+ uri = resolver.expand(uri)
631
+
632
+ if show_graph:
633
+ query = f"""SELECT ?graph ?s ?p ?o WHERE {{
634
+ GRAPH ?graph {{
635
+ {{ <{uri}> ?p ?o . BIND(<{uri}> AS ?s) }}
636
+ UNION
637
+ {{ ?s <{uri}> ?o . BIND(<{uri}> AS ?p) }}
638
+ UNION
639
+ {{ ?s ?p <{uri}> . BIND(<{uri}> AS ?o) }}
640
+ }}
641
+ }} LIMIT {limit}"""
642
+ elif graph_uri:
643
+ query = f"""SELECT ?s ?p ?o WHERE {{
644
+ GRAPH <{graph_uri}> {{
645
+ {{ <{uri}> ?p ?o . BIND(<{uri}> AS ?s) }}
646
+ UNION
647
+ {{ ?s <{uri}> ?o . BIND(<{uri}> AS ?p) }}
648
+ UNION
649
+ {{ ?s ?p <{uri}> . BIND(<{uri}> AS ?o) }}
650
+ }}
651
+ }} LIMIT {limit}"""
652
+ else:
653
+ query = f"""SELECT ?s ?p ?o WHERE {{
654
+ {{ <{uri}> ?p ?o . BIND(<{uri}> AS ?s) }}
655
+ UNION
656
+ {{ ?s <{uri}> ?o . BIND(<{uri}> AS ?p) }}
657
+ UNION
658
+ {{ ?s ?p <{uri}> . BIND(<{uri}> AS ?o) }}
659
+ }} LIMIT {limit}"""
660
+ _execute_convenience_query(query, endpoint, profile, timeout, format, verbose, ctx)
661
+
662
+
663
+ def objects(
664
+ ctx: typer.Context,
665
+ class_uri: str = typer.Argument( # noqa: B008
666
+ ...,
667
+ help="Class URI to list instances of (e.g., dbo:Person, foaf:Agent)",
668
+ ),
669
+ endpoint: str | None = typer.Option( # noqa: B008
670
+ None,
671
+ "--endpoint",
672
+ "-E",
673
+ help="SPARQL endpoint URL (overrides config)",
674
+ ),
675
+ profile: str | None = typer.Option( # noqa: B008
676
+ None,
677
+ "--profile",
678
+ "-P",
679
+ help="Use named endpoint profile from config",
680
+ ),
681
+ limit: int = typer.Option( # noqa: B008
682
+ 100,
683
+ "--limit",
684
+ "-n",
685
+ help="Maximum number of results (default: 100)",
686
+ ),
687
+ timeout: float | None = typer.Option( # noqa: B008
688
+ None,
689
+ "--timeout",
690
+ "-t",
691
+ help="Query timeout in seconds",
692
+ ),
693
+ format: OutputFormat | None = typer.Option( # noqa: B008
694
+ None,
695
+ "--format",
696
+ "-f",
697
+ help="Output format (default: table for TTY, tsv for pipes)",
698
+ ),
699
+ labels: bool = typer.Option( # noqa: B008
700
+ False,
701
+ "--labels/--no-labels",
702
+ "-l",
703
+ help="Include rdfs:label for each object (slower, not all endpoints)",
704
+ ),
705
+ show_graphs: bool = typer.Option( # noqa: B008
706
+ False,
707
+ "--graphs/--no-graphs",
708
+ "-g",
709
+ help="Show graph column in output",
710
+ ),
711
+ graph_filter: str | None = typer.Option( # noqa: B008
712
+ None,
713
+ "--graph",
714
+ "-G",
715
+ help="Filter to specific named graph URI",
716
+ ),
717
+ verbose: bool = typer.Option( # noqa: B008
718
+ False,
719
+ "--verbose",
720
+ "-v",
721
+ help="Show endpoint and query before execution",
722
+ ),
723
+ ) -> None:
724
+ """List instances (objects) of a given RDF class.
725
+
726
+ CLASS_URI can be a full URI or prefixed name (e.g., dbo:Person, foaf:Agent).
727
+
728
+ Use --labels to also fetch rdfs:label for each instance (if available).
729
+ Use -g to show which graph each instance comes from.
730
+ Use -G <uri> to query only that named graph.
731
+ """
732
+ # Merge global graph options (command-specific takes precedence)
733
+ _, _, global_show_graphs, global_graph_filter = _get_global_options(ctx)
734
+ show_graph = show_graphs or global_show_graphs
735
+ graph_uri = graph_filter or global_graph_filter
736
+
737
+ # Expand prefixed names to full URIs
738
+ config = load_config()
739
+ if config.prefixes:
740
+ resolver = PrefixResolver(config.prefixes)
741
+ class_uri = resolver.expand(class_uri)
742
+
743
+ if labels:
744
+ if show_graph:
745
+ query = f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
746
+ SELECT ?graph ?object ?label WHERE {{
747
+ {{
748
+ SELECT DISTINCT ?graph ?object
749
+ WHERE {{ GRAPH ?graph {{ ?object a <{class_uri}> }} }} LIMIT {limit}
750
+ }}
751
+ OPTIONAL {{
752
+ ?object rdfs:label ?label .
753
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
754
+ }}
755
+ }}"""
756
+ elif graph_uri:
757
+ query = f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
758
+ SELECT ?object ?label WHERE {{
759
+ {{
760
+ SELECT DISTINCT ?object
761
+ WHERE {{ GRAPH <{graph_uri}> {{ ?object a <{class_uri}> }} }} LIMIT {limit}
762
+ }}
763
+ OPTIONAL {{
764
+ ?object rdfs:label ?label .
765
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
766
+ }}
767
+ }}"""
768
+ else:
769
+ query = f"""PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
770
+ SELECT ?object ?label WHERE {{
771
+ {{
772
+ SELECT DISTINCT ?object WHERE {{ ?object a <{class_uri}> }} LIMIT {limit}
773
+ }}
774
+ OPTIONAL {{
775
+ ?object rdfs:label ?label .
776
+ FILTER(LANG(?label) = "en" || LANG(?label) = "")
777
+ }}
778
+ }}"""
779
+ else:
780
+ if show_graph:
781
+ query = (
782
+ "SELECT DISTINCT ?graph ?object "
783
+ f"WHERE {{ GRAPH ?graph {{ ?object a <{class_uri}> }} }} LIMIT {limit}"
784
+ )
785
+ elif graph_uri:
786
+ query = (
787
+ "SELECT DISTINCT ?object "
788
+ f"WHERE {{ GRAPH <{graph_uri}> {{ ?object a <{class_uri}> }} }} "
789
+ f"LIMIT {limit}"
790
+ )
791
+ else:
792
+ query = (
793
+ "SELECT DISTINCT ?object "
794
+ f"WHERE {{ ?object a <{class_uri}> }} LIMIT {limit}"
795
+ )
796
+ _execute_convenience_query(query, endpoint, profile, timeout, format, verbose, ctx)
797
+
798
+
799
+ def object(
800
+ ctx: typer.Context,
801
+ uri: str = typer.Argument( # noqa: B008
802
+ ...,
803
+ help="Instance URI to describe (e.g., dbr:Berlin, wd:Q42)",
804
+ ),
805
+ endpoint: str | None = typer.Option( # noqa: B008
806
+ None,
807
+ "--endpoint",
808
+ "-E",
809
+ help="SPARQL endpoint URL (overrides config)",
810
+ ),
811
+ profile: str | None = typer.Option( # noqa: B008
812
+ None,
813
+ "--profile",
814
+ "-P",
815
+ help="Use named endpoint profile from config",
816
+ ),
817
+ limit: int = typer.Option( # noqa: B008
818
+ 100,
819
+ "--limit",
820
+ "-n",
821
+ help="Maximum number of results (default: 100)",
822
+ ),
823
+ timeout: float | None = typer.Option( # noqa: B008
824
+ None,
825
+ "--timeout",
826
+ "-t",
827
+ help="Query timeout in seconds",
828
+ ),
829
+ format: OutputFormat | None = typer.Option( # noqa: B008
830
+ None,
831
+ "--format",
832
+ "-f",
833
+ help="Output format (default: table for TTY, tsv for pipes)",
834
+ ),
835
+ show_graphs: bool = typer.Option( # noqa: B008
836
+ False,
837
+ "--graphs/--no-graphs",
838
+ "-g",
839
+ help="Show graph column in output",
840
+ ),
841
+ graph_filter: str | None = typer.Option( # noqa: B008
842
+ None,
843
+ "--graph",
844
+ "-G",
845
+ help="Filter to specific named graph URI",
846
+ ),
847
+ verbose: bool = typer.Option( # noqa: B008
848
+ False,
849
+ "--verbose",
850
+ "-v",
851
+ help="Show endpoint and query before execution",
852
+ ),
853
+ ) -> None:
854
+ """Show all predicates and values for a specific instance.
855
+
856
+ URI can be a full URI or prefixed name (e.g., dbr:Berlin, wd:Q42).
857
+
858
+ Returns all properties (predicate-value pairs) of the given resource.
859
+ Use -g to see which named graph each triple comes from.
860
+ Use -G <uri> to query only that named graph.
861
+ """
862
+ # Merge global graph options (command-specific takes precedence)
863
+ _, _, global_show_graphs, global_graph_filter = _get_global_options(ctx)
864
+ show_graph = show_graphs or global_show_graphs
865
+ graph_uri = graph_filter or global_graph_filter
866
+
867
+ # Expand prefixed names to full URIs
868
+ config = load_config()
869
+ if config.prefixes:
870
+ resolver = PrefixResolver(config.prefixes)
871
+ uri = resolver.expand(uri)
872
+
873
+ if show_graph:
874
+ query = f"""SELECT DISTINCT ?graph ?predicate ?value WHERE {{
875
+ GRAPH ?graph {{ <{uri}> ?predicate ?value }}
876
+ }} LIMIT {limit}"""
877
+ elif graph_uri:
878
+ query = f"""SELECT DISTINCT ?predicate ?value WHERE {{
879
+ GRAPH <{graph_uri}> {{ <{uri}> ?predicate ?value }}
880
+ }} LIMIT {limit}"""
881
+ else:
882
+ query = (
883
+ "SELECT DISTINCT ?predicate ?value "
884
+ f"WHERE {{ <{uri}> ?predicate ?value }} LIMIT {limit}"
885
+ )
886
+ _execute_convenience_query(query, endpoint, profile, timeout, format, verbose, ctx)