mustrd 0.3.3a1__tar.gz → 0.3.4a1__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 (33) hide show
  1. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/PKG-INFO +79 -16
  2. mustrd-0.3.4a1/README.md +117 -0
  3. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/model/mustrdShapes.ttl +1 -0
  4. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/mustrd.py +28 -48
  5. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/mustrdAnzo.py +15 -6
  6. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/mustrdTestPlugin.py +22 -70
  7. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/spec_component.py +1 -3
  8. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/pyproject.toml +1 -1
  9. mustrd-0.3.3a1/README.md +0 -54
  10. mustrd-0.3.3a1/mustrd/shared_utils.py +0 -0
  11. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/LICENSE +0 -0
  12. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/README.adoc +0 -0
  13. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/README.md +0 -0
  14. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/TestResult.py +0 -0
  15. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/__init__.py +0 -0
  16. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/anzo_utils.py +0 -0
  17. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/logger_setup.py +0 -0
  18. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/model/catalog-v001.xml +0 -0
  19. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/model/mustrdTestOntology.ttl +0 -0
  20. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/model/mustrdTestShapes.ttl +0 -0
  21. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/model/ontology.ttl +0 -0
  22. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/model/test-resources/resources.ttl +0 -0
  23. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/model/triplestoreOntology.ttl +0 -0
  24. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/model/triplestoreshapes.ttl +0 -0
  25. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/mustrdGraphDb.py +0 -0
  26. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/mustrdRdfLib.py +0 -0
  27. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/namespace.py +0 -0
  28. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/run.py +0 -0
  29. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/steprunner.py +0 -0
  30. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/templates/md_ResultList_leaf_template.jinja +0 -0
  31. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/templates/md_ResultList_template.jinja +0 -0
  32. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/templates/md_stats_template.jinja +0 -0
  33. {mustrd-0.3.3a1 → mustrd-0.3.4a1}/mustrd/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mustrd
3
- Version: 0.3.3a1
3
+ Version: 0.3.4a1
4
4
  Summary: A Spec By Example framework for RDF and SPARQL, Inspired by Cucumber.
5
5
  License: MIT
6
6
  Author: John Placek
@@ -34,13 +34,13 @@ Requires-Dist: urllib3 (==1.26.19)
34
34
  Project-URL: Repository, https://github.com/Semantic-partners/mustrd
35
35
  Description-Content-Type: text/markdown
36
36
 
37
- # mustrd
37
+ # MustRD
38
38
 
39
39
  **"MustRD: Validate your SPARQL queries and transformations with precision and confidence, using BDD and Given-When-Then principles."**
40
40
 
41
- [<img src="https://github.com/Semantic-partners/mustrd/raw/python-coverage-comment-action-data/badge.svg?sanitize=true" alt="coverage badge">](https://github.com/Semantic-partners/mustrd/tree/python-coverage-comment-action-data)
41
+ [![Coverage Badge](https://github.com/Semantic-partners/mustrd/raw/python-coverage-comment-action-data/badge.svg?sanitize=true)](https://github.com/Semantic-partners/mustrd/tree/python-coverage-comment-action-data)
42
42
 
43
- ### Why?
43
+ ## Why?
44
44
 
45
45
  SPARQL is a powerful query language for RDF data, but how can you ensure your queries and transformations are doing what you intend? Whether you're working on a pipeline or a standalone query, certainty is key.
46
46
 
@@ -48,11 +48,11 @@ While RDF and SPARQL offer great flexibility, we noticed a gap in tooling to val
48
48
 
49
49
  With MustRD, you can:
50
50
 
51
- * Define data scenarios and verify that queries produce the expected results.
52
- * Test edge cases to ensure your queries remain reliable.
53
- * Isolate small SPARQL enrichment or transformation steps and confirm you're only inserting what you intend.
51
+ - Define data scenarios and verify that queries produce the expected results.
52
+ - Test edge cases to ensure your queries remain reliable.
53
+ - Isolate small SPARQL enrichment or transformation steps and confirm you're only inserting what you intend.
54
54
 
55
- ### What?
55
+ ## What?
56
56
 
57
57
  MustRD is a Spec-By-Example ontology with a reference Python implementation, inspired by tools like Cucumber. It uses the Given-When-Then approach to define and validate SPARQL queries and transformations.
58
58
 
@@ -62,23 +62,85 @@ MustRD is designed to be triplestore/SPARQL engine agnostic, leveraging open sta
62
62
 
63
63
  MustRD is not an alternative to SHACL. While SHACL validates data structures, MustRD focuses on validating data transformations and query results.
64
64
 
65
- ### How?
65
+ ## How?
66
66
 
67
67
  You define your specs in Turtle (`.ttl`) or TriG (`.trig`) files using the Given-When-Then approach:
68
68
 
69
- * **Given**: Define the starting dataset.
70
- * **When**: Specify the action (e.g., a SPARQL query).
71
- * **Then**: Outline the expected results.
69
+ - **Given**: Define the starting dataset.
70
+ - **When**: Specify the action (e.g., a SPARQL query).
71
+ - **Then**: Outline the expected results.
72
72
 
73
73
  Depending on the type of SPARQL query (CONSTRUCT, SELECT, INSERT/DELETE), MustRD runs the query and compares the results against the expectations defined in the spec.
74
74
 
75
75
  Expectations can also be defined as:
76
76
 
77
- * INSERT queries.
78
- * SELECT queries.
79
- * Higher-order expectation languages, similar to those used in various platforms.
77
+ - INSERT queries.
78
+ - SELECT queries.
79
+ - Higher-order expectation languages, similar to those used in various platforms.
80
80
 
81
- ### When?
81
+ ## Example
82
+
83
+ ### Configuration File
84
+
85
+ You'll have a configuration `.ttl` file, which acts as a suite of tests. It tells MustRD where to look for test specifications and any triplestore configurations you might have:
86
+
87
+ ```ttl
88
+ :test_example a :MustrdTest;
89
+ :hasSpecPath "test/specs/";
90
+ :hasDataPath "test/data/";
91
+ :hasPytestPath "example";
92
+ :triplestoreSpecPath "test/triplestore_config/triplestores.ttl";
93
+ :filterOnTripleStore triplestore:example_test .
94
+ ```
95
+
96
+ ### Test Specification
97
+
98
+ In the directory specified by `:hasSpecPath`, you'll have one or more `.mustrd.ttl` files. These can be organized in a directory structure. MustRD collects them and reports results to your test runner.
99
+
100
+ ```ttl
101
+ :test_example :given [ a :FileDataset ;
102
+ :file "test/data/given.ttl" ] ;
103
+ :when [ a :TextSparqlSource ;
104
+ :queryText "SELECT ?s ?p ?o WHERE { ?s ?p ?o }" ;
105
+ :queryType :SelectSparql ] ;
106
+ :then [ a :OrderedTableDataset ;
107
+ :hasRow [ :variable "s" ; :boundValue "example:subject" ;
108
+ :variable "p" ; :boundValue "example:predicate" ;
109
+ :variable "o" ; :boundValue "example:object" ] ].
110
+ ```
111
+
112
+ And you will have a `'test/data/given.ttl'` which contains the given ttl.
113
+
114
+ ```ttl
115
+ example:subject example:predicate example:object .
116
+ ```
117
+
118
+ ### Running Tests
119
+
120
+ Run the test using the MustRD Pytest plugin:
121
+
122
+ ```bash
123
+ poetry run pytest --mustrd --config=test/mustrd_configuration.ttl --md=render/github_job_summary.md
124
+ ```
125
+
126
+ This will validate your SPARQL queries against the defined dataset and expected results, ensuring your transformations behave as intended.
127
+
128
+ You can refer to SPARQL inline, in files, or in Anzo Graphmarts, Steps, or Layers. See `GETSTARTED.adoc` for more details.
129
+
130
+ #### Integrating with Visual Studio Code (vscode)
131
+ We have a pytest plugin.
132
+ 1. Choose a python interpreter (probably a venv)
133
+ 2. `pip install mustrd ` in it.
134
+ 3. add to your settings.json
135
+ ```json
136
+ "python.testing.pytestArgs": [
137
+ "--mustrd", "--md=junit/github_job_summary.md", "--config=test/test_config_local.ttl"
138
+ ],
139
+ ```
140
+ 4. VS Code should auto discover your tests and they'll show up in the flask icon 'tab'.
141
+ ![alt text](image.png)
142
+
143
+ ## When?
82
144
 
83
145
  MustRD is a work in progress, built to meet the needs of our projects across multiple clients and vendor stacks. While we find it useful, it may not meet your needs out of the box.
84
146
 
@@ -89,3 +151,4 @@ We invite you to try it, raise issues, or contribute via pull requests. If you n
89
151
  Semantic Partners is a specialist consultancy in Semantic Technology. If you need more support, contact us at info@semanticpartners.com or mustrd@semanticpartners.com.
90
152
 
91
153
 
154
+
@@ -0,0 +1,117 @@
1
+ # MustRD
2
+
3
+ **"MustRD: Validate your SPARQL queries and transformations with precision and confidence, using BDD and Given-When-Then principles."**
4
+
5
+ [![Coverage Badge](https://github.com/Semantic-partners/mustrd/raw/python-coverage-comment-action-data/badge.svg?sanitize=true)](https://github.com/Semantic-partners/mustrd/tree/python-coverage-comment-action-data)
6
+
7
+ ## Why?
8
+
9
+ SPARQL is a powerful query language for RDF data, but how can you ensure your queries and transformations are doing what you intend? Whether you're working on a pipeline or a standalone query, certainty is key.
10
+
11
+ While RDF and SPARQL offer great flexibility, we noticed a gap in tooling to validate their behavior. We missed the robust testing frameworks available in imperative programming languages that help ensure your code works as expected.
12
+
13
+ With MustRD, you can:
14
+
15
+ - Define data scenarios and verify that queries produce the expected results.
16
+ - Test edge cases to ensure your queries remain reliable.
17
+ - Isolate small SPARQL enrichment or transformation steps and confirm you're only inserting what you intend.
18
+
19
+ ## What?
20
+
21
+ MustRD is a Spec-By-Example ontology with a reference Python implementation, inspired by tools like Cucumber. It uses the Given-When-Then approach to define and validate SPARQL queries and transformations.
22
+
23
+ MustRD is designed to be triplestore/SPARQL engine agnostic, leveraging open standards to ensure compatibility across different platforms.
24
+
25
+ ### What it is NOT
26
+
27
+ MustRD is not an alternative to SHACL. While SHACL validates data structures, MustRD focuses on validating data transformations and query results.
28
+
29
+ ## How?
30
+
31
+ You define your specs in Turtle (`.ttl`) or TriG (`.trig`) files using the Given-When-Then approach:
32
+
33
+ - **Given**: Define the starting dataset.
34
+ - **When**: Specify the action (e.g., a SPARQL query).
35
+ - **Then**: Outline the expected results.
36
+
37
+ Depending on the type of SPARQL query (CONSTRUCT, SELECT, INSERT/DELETE), MustRD runs the query and compares the results against the expectations defined in the spec.
38
+
39
+ Expectations can also be defined as:
40
+
41
+ - INSERT queries.
42
+ - SELECT queries.
43
+ - Higher-order expectation languages, similar to those used in various platforms.
44
+
45
+ ## Example
46
+
47
+ ### Configuration File
48
+
49
+ You'll have a configuration `.ttl` file, which acts as a suite of tests. It tells MustRD where to look for test specifications and any triplestore configurations you might have:
50
+
51
+ ```ttl
52
+ :test_example a :MustrdTest;
53
+ :hasSpecPath "test/specs/";
54
+ :hasDataPath "test/data/";
55
+ :hasPytestPath "example";
56
+ :triplestoreSpecPath "test/triplestore_config/triplestores.ttl";
57
+ :filterOnTripleStore triplestore:example_test .
58
+ ```
59
+
60
+ ### Test Specification
61
+
62
+ In the directory specified by `:hasSpecPath`, you'll have one or more `.mustrd.ttl` files. These can be organized in a directory structure. MustRD collects them and reports results to your test runner.
63
+
64
+ ```ttl
65
+ :test_example :given [ a :FileDataset ;
66
+ :file "test/data/given.ttl" ] ;
67
+ :when [ a :TextSparqlSource ;
68
+ :queryText "SELECT ?s ?p ?o WHERE { ?s ?p ?o }" ;
69
+ :queryType :SelectSparql ] ;
70
+ :then [ a :OrderedTableDataset ;
71
+ :hasRow [ :variable "s" ; :boundValue "example:subject" ;
72
+ :variable "p" ; :boundValue "example:predicate" ;
73
+ :variable "o" ; :boundValue "example:object" ] ].
74
+ ```
75
+
76
+ And you will have a `'test/data/given.ttl'` which contains the given ttl.
77
+
78
+ ```ttl
79
+ example:subject example:predicate example:object .
80
+ ```
81
+
82
+ ### Running Tests
83
+
84
+ Run the test using the MustRD Pytest plugin:
85
+
86
+ ```bash
87
+ poetry run pytest --mustrd --config=test/mustrd_configuration.ttl --md=render/github_job_summary.md
88
+ ```
89
+
90
+ This will validate your SPARQL queries against the defined dataset and expected results, ensuring your transformations behave as intended.
91
+
92
+ You can refer to SPARQL inline, in files, or in Anzo Graphmarts, Steps, or Layers. See `GETSTARTED.adoc` for more details.
93
+
94
+ #### Integrating with Visual Studio Code (vscode)
95
+ We have a pytest plugin.
96
+ 1. Choose a python interpreter (probably a venv)
97
+ 2. `pip install mustrd ` in it.
98
+ 3. add to your settings.json
99
+ ```json
100
+ "python.testing.pytestArgs": [
101
+ "--mustrd", "--md=junit/github_job_summary.md", "--config=test/test_config_local.ttl"
102
+ ],
103
+ ```
104
+ 4. VS Code should auto discover your tests and they'll show up in the flask icon 'tab'.
105
+ ![alt text](image.png)
106
+
107
+ ## When?
108
+
109
+ MustRD is a work in progress, built to meet the needs of our projects across multiple clients and vendor stacks. While we find it useful, it may not meet your needs out of the box.
110
+
111
+ We invite you to try it, raise issues, or contribute via pull requests. If you need custom features, contact us for consultancy rates, and we may prioritize your request.
112
+
113
+ ## Support
114
+
115
+ Semantic Partners is a specialist consultancy in Semantic Technology. If you need more support, contact us at info@semanticpartners.com or mustrd@semanticpartners.com.
116
+
117
+
@@ -169,6 +169,7 @@ must:SparqlSourceShape
169
169
  a sh:NodeShape ;
170
170
  sh:targetClass must:SparqlSource ;
171
171
  sh:property [ sh:path must:queryType ;
172
+ sh:in ( must:SelectSparql must:ConstructSparql must:UpdateSparql) ;
172
173
  sh:minCount 1 ;
173
174
  sh:maxCount 1 ; ] .
174
175
 
@@ -134,7 +134,7 @@ class TripleStoreConnectionError(SpecResult):
134
134
 
135
135
 
136
136
  @dataclass
137
- class SpecSkipped(SpecResult):
137
+ class SpecInvalid(SpecResult):
138
138
  message: str
139
139
  spec_file_name: str = "default.mustrd.ttl"
140
140
  spec_source_file: Path = Path("default.mustrd.ttl")
@@ -204,6 +204,12 @@ def validate_specs(
204
204
  f"Could not extract spec from {file} due to exception of type "
205
205
  f"{type(e).__name__} when parsing file"
206
206
  ]
207
+ invalid_specs += [
208
+ SpecInvalid(
209
+ "urn:invalid_spec_file", triple_store["type"], message, file.name, file
210
+ )
211
+ for triple_store in triple_stores
212
+ ]
207
213
  continue
208
214
 
209
215
  # run shacl validation
@@ -310,7 +316,7 @@ def add_spec_validation(
310
316
  error_messages.sort()
311
317
  error_message = "\n".join(msg for msg in error_messages)
312
318
  invalid_specs += [
313
- SpecSkipped(
319
+ SpecInvalid(
314
320
  subject_uri, triple_store["type"], error_message, file.name, file
315
321
  )
316
322
  for triple_store in triple_stores
@@ -324,15 +330,15 @@ def get_specs(
324
330
  run_config: dict,
325
331
  ):
326
332
  specs = []
327
- skipped_results = []
333
+ invalid_spec = []
328
334
  try:
329
335
  for triple_store in triple_stores:
330
336
  if "error" in triple_store:
331
337
  log.error(
332
338
  f"{triple_store['error']}. No specs run for this triple store."
333
339
  )
334
- skipped_results += [
335
- SpecSkipped(
340
+ invalid_spec += [
341
+ SpecInvalid(
336
342
  spec_uri,
337
343
  triple_store["type"],
338
344
  triple_store["error"],
@@ -360,8 +366,8 @@ def get_specs(
360
366
  )
361
367
  or "unknown"
362
368
  )
363
- skipped_results += [
364
- SpecSkipped(
369
+ invalid_spec += [
370
+ SpecInvalid(
365
371
  spec_uri,
366
372
  triple_store["type"],
367
373
  str(e),
@@ -380,7 +386,7 @@ def get_specs(
380
386
  log.error("No specifications will be run.")
381
387
 
382
388
  log.info(f"Extracted {len(specs)} specifications that will be run")
383
- return specs, skipped_results
389
+ return specs, invalid_spec
384
390
 
385
391
 
386
392
  def run_specs(specs) -> List[SpecResult]:
@@ -505,7 +511,7 @@ def run_spec(spec: Specification) -> SpecResult:
505
511
  upload_given(triple_store, spec.given)
506
512
  else:
507
513
  if triple_store["type"] == TRIPLESTORE.RdfLib:
508
- return SpecSkipped(
514
+ return SpecInvalid(
509
515
  spec_uri,
510
516
  triple_store["type"],
511
517
  "Unable to run Inherited State tests on Rdflib",
@@ -526,7 +532,6 @@ def run_spec(spec: Specification) -> SpecResult:
526
532
  except NotImplementedError as ex:
527
533
  log.error(f"NotImplementedError {ex}")
528
534
  raise ex
529
- # return SpecSkipped(spec_uri, triple_store["type"], ex.args[0])
530
535
  return check_result(spec, result)
531
536
  except (ConnectionError, TimeoutError, HTTPError, ConnectTimeout, OSError) as e:
532
537
  # close_connection = False
@@ -548,38 +553,13 @@ def run_spec(spec: Specification) -> SpecResult:
548
553
 
549
554
 
550
555
  def get_triple_store_graph(triple_store_graph_path: Path, secrets: str):
551
- graph = Graph()
552
- # Parse the main triple store graph file
553
- try:
554
- graph.parse(triple_store_graph_path)
555
- except Exception as e:
556
- log.error(f"Failed to parse triple store graph file '{triple_store_graph_path}': {e}")
557
- raise
558
-
559
- # Parse secrets, either from string or from file
560
556
  if secrets:
561
- log.info("Parsing secrets from provided string (--secrets option)")
562
- log.info("" + secrets)
563
- try:
564
- graph.parse(data=secrets)
565
- except Exception as e:
566
- log.error(f"Failed to parse secrets data for triple store graph: {e}")
567
- raise
557
+ return Graph().parse(triple_store_graph_path).parse(data=secrets)
568
558
  else:
569
- secret_path = triple_store_graph_path.with_name(
559
+ secret_path = triple_store_graph_path.parent / Path(
570
560
  triple_store_graph_path.stem + "_secrets" + triple_store_graph_path.suffix
571
561
  )
572
- log.info("Parsing secrets from secrets file: " + str(secret_path))
573
- if secret_path.exists():
574
- try:
575
- graph.parse(secret_path)
576
- except Exception as e:
577
- log.error(f"Failed to parse secrets file '{secret_path}': {e}")
578
- raise
579
- else:
580
- log.info(f"No secrets file found at '{secret_path}', continuing without it.")
581
-
582
- return graph
562
+ return Graph().parse(triple_store_graph_path).parse(secret_path)
583
563
 
584
564
 
585
565
  # Parse and validate triple store configuration
@@ -973,8 +953,8 @@ def write_result_diff_to_log(res, info):
973
953
  ):
974
954
  info(f"{Fore.RED}Failed {res.spec_uri} {res.triple_store}")
975
955
  info(res.exception)
976
- if isinstance(res, SpecSkipped):
977
- info(f"{Fore.YELLOW}Skipped {res.spec_uri} {res.triple_store}")
956
+ if isinstance(res, SpecInvalid):
957
+ info(f"{Fore.RED} Invalid {res.spec_uri} {res.triple_store}")
978
958
  info(res.message)
979
959
 
980
960
 
@@ -1071,7 +1051,7 @@ def review_results(results: List[SpecResult], verbose: bool) -> None:
1071
1051
  colours = {
1072
1052
  SpecPassed: Fore.GREEN,
1073
1053
  SpecPassedWithWarning: Fore.YELLOW,
1074
- SpecSkipped: Fore.YELLOW,
1054
+ SpecInvalid: Fore.RED,
1075
1055
  }
1076
1056
  # Populate dictionaries from results
1077
1057
  for result in results:
@@ -1128,12 +1108,12 @@ def review_results(results: List[SpecResult], verbose: bool) -> None:
1128
1108
 
1129
1109
  pass_count = statuses.count(SpecPassed)
1130
1110
  warning_count = statuses.count(SpecPassedWithWarning)
1131
- skipped_count = statuses.count(SpecSkipped)
1111
+ invalid_count = statuses.count(SpecInvalid)
1132
1112
  fail_count = len(
1133
1113
  list(
1134
1114
  filter(
1135
1115
  lambda status: status
1136
- not in [SpecPassed, SpecPassedWithWarning, SpecSkipped],
1116
+ not in [SpecPassed, SpecPassedWithWarning, SpecInvalid],
1137
1117
  statuses,
1138
1118
  )
1139
1119
  )
@@ -1141,18 +1121,18 @@ def review_results(results: List[SpecResult], verbose: bool) -> None:
1141
1121
 
1142
1122
  if fail_count:
1143
1123
  overview_colour = Fore.RED
1144
- elif warning_count or skipped_count:
1124
+ elif warning_count or invalid_count:
1145
1125
  overview_colour = Fore.YELLOW
1146
1126
  else:
1147
1127
  overview_colour = Fore.GREEN
1148
1128
 
1149
1129
  logger_setup.flush()
1150
1130
  log.info(
1151
- f"{overview_colour}===== {fail_count} failures, {skipped_count} skipped, {Fore.GREEN}{pass_count} passed, "
1131
+ f"{overview_colour}===== {fail_count} failures, {invalid_count} invalid, {Fore.GREEN}{pass_count} passed, "
1152
1132
  f"{overview_colour}{warning_count} passed with warnings ====="
1153
1133
  )
1154
1134
 
1155
- if verbose and (fail_count or warning_count or skipped_count):
1135
+ if verbose and (fail_count or warning_count or invalid_count):
1156
1136
  display_verbose(results)
1157
1137
 
1158
1138
 
@@ -1190,8 +1170,8 @@ def display_verbose(results: List[SpecResult]):
1190
1170
  ):
1191
1171
  log.info(f"{Fore.RED}Failed {res.spec_uri} {res.triple_store}")
1192
1172
  log.info(res.exception)
1193
- if isinstance(res, SpecSkipped):
1194
- log.info(f"{Fore.YELLOW}Skipped {res.spec_uri} {res.triple_store}")
1173
+ if isinstance(res, SpecInvalid):
1174
+ log.info(f"{Fore.YELLOW}Invalid {res.spec_uri} {res.triple_store}")
1195
1175
  log.info(res.message)
1196
1176
 
1197
1177
 
@@ -98,8 +98,11 @@ def get_query_from_step(triple_store: dict, query_step_uri: URIRef) -> str:
98
98
  ?stepUri a <http://cambridgesemantics.com/ontologies/Graphmarts#Step>;
99
99
  <http://cambridgesemantics.com/ontologies/Graphmarts#transformQuery> ?query
100
100
  }}"""
101
- return json_to_dictlist(query_configuration(anzo_config=triple_store, query=query))[0]['query']
102
-
101
+ result = json_to_dictlist(query_configuration(anzo_config=triple_store, query=query))
102
+ if len(result) == 0:
103
+ raise FileNotFoundError(
104
+ f"Querynot found for step {query_step_uri}")
105
+ return result[0].get("query")
103
106
 
104
107
  def get_queries_from_templated_step(triple_store: dict, query_step_uri: URIRef) -> dict:
105
108
  query = f"""SELECT ?param_query ?query_template WHERE {{
@@ -109,8 +112,11 @@ def get_queries_from_templated_step(triple_store: dict, query_step_uri: URIRef)
109
112
  <http://cambridgesemantics.com/ontologies/Graphmarts#template> ?query_template .
110
113
  }}
111
114
  """
112
- return json_to_dictlist(query_configuration(anzo_config=triple_store, query=query))[0]
113
-
115
+ result = json_to_dictlist(query_configuration(anzo_config=triple_store, query=query))
116
+ if len(result) == 0:
117
+ raise FileNotFoundError(
118
+ f"Templated query not found for {query_step_uri}")
119
+ return result[0]
114
120
 
115
121
  def get_queries_for_layer(triple_store: dict, graphmart_layer_uri: URIRef):
116
122
  query = f"""PREFIX graphmarts: <http://cambridgesemantics.com/ontologies/Graphmarts#>
@@ -129,8 +135,11 @@ SELECT ?query ?param_query ?query_template
129
135
  . }}
130
136
  }}
131
137
  ORDER BY ?index"""
132
- return json_to_dictlist(query_configuration(anzo_config=triple_store, query=query))
133
-
138
+ result = json_to_dictlist(query_configuration(anzo_config=triple_store, query=query))
139
+ if len(result) == 0:
140
+ raise FileNotFoundError(
141
+ f"Queries not found for graphmart layer {graphmart_layer_uri}")
142
+ return result
134
143
 
135
144
  def upload_given(triple_store: dict, given: Graph):
136
145
  logging.debug(f"upload_given {triple_store} {given}")
@@ -2,7 +2,7 @@ import logging
2
2
  from dataclasses import dataclass
3
3
  import pytest
4
4
  import os
5
- from pathlib import Path, PosixPath
5
+ from pathlib import Path
6
6
  from rdflib.namespace import Namespace
7
7
  from rdflib import Graph, RDF
8
8
  from pytest import Session
@@ -11,20 +11,16 @@ from mustrd import logger_setup
11
11
  from mustrd.TestResult import ResultList, TestResult, get_result_list
12
12
  from mustrd.utils import get_mustrd_root
13
13
  from mustrd.mustrd import (
14
- write_result_diff_to_log,
15
- get_triple_store_graph,
16
- get_triple_stores,
17
- )
18
- from mustrd.mustrd import (
19
- Specification,
20
- SpecSkipped,
21
14
  validate_specs,
22
15
  get_specs,
23
16
  SpecPassed,
24
17
  run_spec,
18
+ write_result_diff_to_log,
19
+ get_triple_store_graph,
20
+ get_triple_stores,
21
+ SpecInvalid
25
22
  )
26
23
  from mustrd.namespace import MUST, TRIPLESTORE, MUSTRDTEST
27
- from typing import Union
28
24
  from pyshacl import validate
29
25
 
30
26
  import pathlib
@@ -171,13 +167,6 @@ class TestConfig:
171
167
  filter_on_tripleStore: str = None
172
168
 
173
169
 
174
- @dataclass(frozen=True)
175
- class TestParamWrapper:
176
- id: str
177
- test_config: TestConfig
178
- unit_test: Union[Specification, SpecSkipped]
179
-
180
-
181
170
  # Configure logging
182
171
  logger = logger_setup.setup_logger(__name__)
183
172
 
@@ -187,7 +176,6 @@ class MustrdTestPlugin:
187
176
  test_config_file: Path
188
177
  selected_tests: list
189
178
  secrets: str
190
- unit_tests: Union[Specification, SpecSkipped]
191
179
  items: list
192
180
  path_filter: str
193
181
  collect_error: BaseException
@@ -201,18 +189,17 @@ class MustrdTestPlugin:
201
189
  @pytest.hookimpl(tryfirst=True)
202
190
  def pytest_collection(self, session):
203
191
  logger.info("Starting test collection")
204
-
205
- self.unit_tests = []
192
+
206
193
  args = session.config.args
207
-
194
+
208
195
  # Split args into mustrd and regular pytest args
209
196
  mustrd_args = [arg for arg in args if ".mustrd.ttl" in arg]
210
197
  pytest_args = [arg for arg in args if arg != os.getcwd() and ".mustrd.ttl" not in arg]
211
-
198
+
212
199
  self.selected_tests = list(
213
200
  map(
214
201
  lambda arg: Path(arg.split("::")[0]),
215
- mustrd_args
202
+ mustrd_args
216
203
  )
217
204
  )
218
205
  logger.info(f"selected_tests is: {self.selected_tests}")
@@ -237,7 +224,7 @@ class MustrdTestPlugin:
237
224
 
238
225
  def get_file_name_from_arg(self, arg):
239
226
  if arg and len(arg) > 0 and "[" in arg and ".mustrd.ttl " in arg:
240
- return arg[arg.index("[") + 1 : arg.index(".mustrd.ttl ")]
227
+ return arg[arg.index("[") + 1: arg.index(".mustrd.ttl ")]
241
228
  return None
242
229
 
243
230
  @pytest.hookimpl
@@ -247,9 +234,9 @@ class MustrdTestPlugin:
247
234
  if not str(path).endswith('.ttl'):
248
235
  return None
249
236
  if Path(path).resolve() != Path(self.test_config_file).resolve():
250
- logger.debug(f"{self.test_config_file}: Skipping non-matching-config file: {path}")
251
- return None
252
-
237
+ logger.debug(f"{self.test_config_file}: Skipping non-matching-config file: {path}")
238
+ return None
239
+
253
240
  mustrd_file = MustrdFile.from_parent(parent, path=pathlib.Path(path), mustrd_plugin=self)
254
241
  mustrd_file.mustrd_plugin = self
255
242
  return mustrd_file
@@ -264,7 +251,7 @@ class MustrdTestPlugin:
264
251
  logger.debug("Generating tests for config: " + str(config))
265
252
  logger.debug(f"selected_tests {self.selected_tests}")
266
253
 
267
- valid_spec_uris, spec_graph, invalid_spec_results = validate_specs(
254
+ valid_spec_uris, spec_graph, invalid_specs = validate_specs(
268
255
  config,
269
256
  triple_stores,
270
257
  shacl_graph,
@@ -272,17 +259,6 @@ class MustrdTestPlugin:
272
259
  file_name or "*",
273
260
  selected_test_files=self.selected_tests,
274
261
  )
275
- # Convert invalid specs to SpecInvalid instead of SpecSkipped
276
- invalid_specs = [
277
- SpecInvalid(
278
- spec.spec_uri,
279
- spec.triple_store,
280
- spec.message,
281
- spec.spec_file_name,
282
- spec.spec_source_file
283
- ) for spec in invalid_spec_results
284
- ]
285
-
286
262
 
287
263
  specs, skipped_spec_results = get_specs(
288
264
  valid_spec_uris, spec_graph, triple_stores, config
@@ -291,18 +267,6 @@ class MustrdTestPlugin:
291
267
  # Return normal specs + skipped results
292
268
  return specs + skipped_spec_results + invalid_specs
293
269
 
294
- # Function called to generate the name of the test
295
- def get_test_name(self, spec):
296
- # FIXME: SpecSkipped should have the same structure?
297
- if isinstance(spec, SpecSkipped):
298
- triple_store = spec.triple_store
299
- else:
300
- triple_store = spec.triple_store["type"]
301
-
302
- triple_store_name = triple_store.replace("https://mustrd.com/model/", "")
303
- test_name = spec.spec_uri.replace(spnamespace, "").replace("_", " ")
304
- return spec.spec_file_name + " : " + triple_store_name + ": " + test_name
305
-
306
270
  # Get triple store configuration or default
307
271
  def get_triple_stores_from_file(self, test_config):
308
272
  if test_config.triplestore_spec_path:
@@ -397,13 +361,6 @@ class MustrdTestPlugin:
397
361
  with open(self.md_path, "w") as file:
398
362
  file.write(md)
399
363
 
400
- @dataclass(frozen=True)
401
- class SpecInvalid:
402
- spec_uri: str
403
- triple_store: str
404
- message: str
405
- spec_file_name: str = None
406
- spec_source_file: Path = None
407
364
 
408
365
  class MustrdFile(pytest.File):
409
366
  mustrd_plugin: MustrdTestPlugin
@@ -417,14 +374,14 @@ class MustrdFile(pytest.File):
417
374
  try:
418
375
  logger.info(f"{self.mustrd_plugin.test_config_file}: Collecting tests from file: {self.path=}")
419
376
  # Only process the specific mustrd config file we were given
420
-
377
+
421
378
  # if not str(self.fspath).endswith(".ttl"):
422
379
  # return []
423
380
  # Only process the specific mustrd config file we were given
424
381
  # if str(self.fspath) != str(self.mustrd_plugin.test_config_file):
425
382
  # logger.info(f"Skipping non-config file: {self.fspath}")
426
383
  # return []
427
-
384
+
428
385
  test_configs = parse_config(self.path)
429
386
  from collections import defaultdict
430
387
  pytest_path_grouped = defaultdict(list)
@@ -435,7 +392,7 @@ class MustrdFile(pytest.File):
435
392
  ):
436
393
  logger.info(f"Skipping test config due to path filter: {test_config.pytest_path=} {self.mustrd_plugin.path_filter=}")
437
394
  continue
438
-
395
+
439
396
  triple_stores = self.mustrd_plugin.get_triple_stores_from_file(test_config)
440
397
  try:
441
398
  specs = self.mustrd_plugin.generate_tests_for_config(
@@ -521,7 +478,7 @@ class MustrdItem(pytest.Item):
521
478
  f"Error: \n{excinfo.value}\n"
522
479
  f"Traceback:\n{tb_str}"
523
480
  )
524
-
481
+
525
482
  def reportinfo(self):
526
483
  r = "", 0, f"mustrd test: {self.name}"
527
484
  return r
@@ -531,9 +488,6 @@ class MustrdItem(pytest.Item):
531
488
  def run_test_spec(test_spec):
532
489
  logger = logging.getLogger("mustrd.test")
533
490
  logger.info(f"Running test spec: {getattr(test_spec, 'spec_uri', test_spec)}")
534
- if isinstance(test_spec, SpecSkipped):
535
- logger.warning(f"Test skipped: {test_spec.message}")
536
- pytest.skip(f"Invalid configuration, error : {test_spec.message}")
537
491
  try:
538
492
  result = run_spec(test_spec)
539
493
  logger.info(f"Result type: {type(result)} for spec: {getattr(test_spec, 'spec_uri', test_spec)}")
@@ -544,13 +498,11 @@ def run_test_spec(test_spec):
544
498
 
545
499
  if isinstance(test_spec, SpecInvalid):
546
500
  logger.error(f"Invalid test specification: {test_spec.message} {test_spec}")
547
- raise ValueError(f"Invalid test specification: {test_spec.message} {test_spec}")
548
- if type(result) == SpecSkipped:
549
- logger.warning("Test skipped due to unsupported configuration")
550
- pytest.skip("Unsupported configuration")
551
- if type(result) != SpecPassed:
501
+ pytest.fail(f"Invalid test specification: {test_spec.message} {test_spec}")
502
+ if not isinstance(result, SpecPassed):
552
503
  write_result_diff_to_log(result, logger.info)
553
504
  log_lines = []
505
+
554
506
  def log_to_string(message):
555
507
  log_lines.append(message)
556
508
  try:
@@ -562,4 +514,4 @@ def run_test_spec(test_spec):
562
514
  raise AssertionError("Test failed: " + "\n".join(log_lines))
563
515
 
564
516
  logger.info(f"Test PASSED: {getattr(test_spec, 'spec_uri', test_spec)}")
565
- return type(result) == SpecPassed
517
+ return isinstance(result, SpecPassed)
@@ -179,8 +179,6 @@ def get_file_absolute_path(spec_component_details: SpecComponentDetails, relativ
179
179
 
180
180
 
181
181
  def get_spec_component_type(spec_components: List[SpecComponent]) -> Type[SpecComponent]:
182
- if not spec_components:
183
- raise ValueError("spec_components list is empty")
184
182
  # Get the type of the first object in the list
185
183
  spec_type = type(spec_components[0])
186
184
  # Loop through the remaining objects in the list and check their types
@@ -677,7 +675,7 @@ def get_spec_component_from_file(path: Path) -> str:
677
675
  raise ValueError(f"Path {path} is a directory, expected a file")
678
676
 
679
677
  try:
680
- content = path.read_text()
678
+ content = path.read_text(encoding='utf-8')
681
679
  except FileNotFoundError:
682
680
  raise
683
681
  return str(content)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mustrd"
3
- version = "0.3.3a1"
3
+ version = "0.3.4a1"
4
4
  description = "A Spec By Example framework for RDF and SPARQL, Inspired by Cucumber."
5
5
  authors = [
6
6
  { name = "John Placek", email = "john.placek@semanticpartners.com" },
mustrd-0.3.3a1/README.md DELETED
@@ -1,54 +0,0 @@
1
- # mustrd
2
-
3
- **"MustRD: Validate your SPARQL queries and transformations with precision and confidence, using BDD and Given-When-Then principles."**
4
-
5
- [<img src="https://github.com/Semantic-partners/mustrd/raw/python-coverage-comment-action-data/badge.svg?sanitize=true" alt="coverage badge">](https://github.com/Semantic-partners/mustrd/tree/python-coverage-comment-action-data)
6
-
7
- ### Why?
8
-
9
- SPARQL is a powerful query language for RDF data, but how can you ensure your queries and transformations are doing what you intend? Whether you're working on a pipeline or a standalone query, certainty is key.
10
-
11
- While RDF and SPARQL offer great flexibility, we noticed a gap in tooling to validate their behavior. We missed the robust testing frameworks available in imperative programming languages that help ensure your code works as expected.
12
-
13
- With MustRD, you can:
14
-
15
- * Define data scenarios and verify that queries produce the expected results.
16
- * Test edge cases to ensure your queries remain reliable.
17
- * Isolate small SPARQL enrichment or transformation steps and confirm you're only inserting what you intend.
18
-
19
- ### What?
20
-
21
- MustRD is a Spec-By-Example ontology with a reference Python implementation, inspired by tools like Cucumber. It uses the Given-When-Then approach to define and validate SPARQL queries and transformations.
22
-
23
- MustRD is designed to be triplestore/SPARQL engine agnostic, leveraging open standards to ensure compatibility across different platforms.
24
-
25
- ### What it is NOT
26
-
27
- MustRD is not an alternative to SHACL. While SHACL validates data structures, MustRD focuses on validating data transformations and query results.
28
-
29
- ### How?
30
-
31
- You define your specs in Turtle (`.ttl`) or TriG (`.trig`) files using the Given-When-Then approach:
32
-
33
- * **Given**: Define the starting dataset.
34
- * **When**: Specify the action (e.g., a SPARQL query).
35
- * **Then**: Outline the expected results.
36
-
37
- Depending on the type of SPARQL query (CONSTRUCT, SELECT, INSERT/DELETE), MustRD runs the query and compares the results against the expectations defined in the spec.
38
-
39
- Expectations can also be defined as:
40
-
41
- * INSERT queries.
42
- * SELECT queries.
43
- * Higher-order expectation languages, similar to those used in various platforms.
44
-
45
- ### When?
46
-
47
- MustRD is a work in progress, built to meet the needs of our projects across multiple clients and vendor stacks. While we find it useful, it may not meet your needs out of the box.
48
-
49
- We invite you to try it, raise issues, or contribute via pull requests. If you need custom features, contact us for consultancy rates, and we may prioritize your request.
50
-
51
- ## Support
52
-
53
- Semantic Partners is a specialist consultancy in Semantic Technology. If you need more support, contact us at info@semanticpartners.com or mustrd@semanticpartners.com.
54
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes