Skip to content

Commit d3dc446

Browse files
authored
Merge pull request #19 from AKSW/askretries
Askretries
2 parents 3411786 + bc4473a commit d3dc446

10 files changed

Lines changed: 176 additions & 40 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
55

66
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/)
77

8+
## [2.1.0] 2026-02-24
9+
10+
### Added
11+
12+
-ask command
13+
- new options to control amount of `--retries` and the amount fo `--retry-sleep` between retries
14+
- all logs from retries and eventual skips are logged into the `--retries-log`
15+
- tests
16+
- added tests for the new retry options and logging
17+
18+
### Changed
19+
20+
- serve command
21+
- default waiting time for answer requests is now 3 seconds instead of 0 to allow for timeout logging tests to work properly
22+
- fixes
23+
- fixed a testing bug where after Click 8.2.0 the stdout and stderr were not properly captured in the tests with error assertion
24+
825
## [2.0.0] 2026-02-11
926

1027
### Added

‎README.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,20 @@ The TEXT2SPARQL-CLIENT is the official testing CLI tool for the [International T
2727

2828
### CK25
2929

30-
Usage example for the CK25 dataset from the [First International TEXT2SPARQL challenge](https://text2sparql.aksw.org/2025/). To execute tests for the CK25 dataset you will need a valid API according to the challenge specifications, [see](https://text2sparql.aksw.org/latest/participation/challenge/#process) and the directory `examples`. It contains the questions file `questions_ck25.yml` the True Result-set `ck25_true_result.json` (which can be generated with the questions file running the command `text2sparql query -o "ck25_true_result.json" questions.yml`), and the example shell file. To ask, query and evaluate according to your API open the `examples` directory and run `run_ck25.sh` passing `<API_IP> <API_NAME>`, for example:
30+
Usage example for the CK25 dataset from the [First International TEXT2SPARQL challenge](https://text2sparql.aksw.org/2025/). To execute tests for the CK25 dataset you will need a valid API according to the challenge specifications, [see](https://text2sparql.aksw.org/latest/participation/challenge/#process) and the directory `examples`. It contains the questions file `questions_ck25.yml` the True Result-set `ck25_true_result.json` (which can be generated with the questions file running the command `text2sparql query -o "ck25_true_result.json" questions.yml`), and the example shell file. To ask, query and evaluate according to your API open the `examples` directory and run `run_ck25.sh` passing `<API_URL> <API_NAME>`, for example:
3131

3232
```bash
3333
cd examples
34-
bash run_ck25.sh "http://127.0.0.1:8000" "text2sparql_qwen3"
34+
bash run_ck25.sh "http://localhost:8000" "text2sparql"
3535
```
3636

3737
### DB25
3838

39-
Usage example for the DB25 dataset from the [First International TEXT2SPARQL challenge](https://text2sparql.aksw.org/2025/). To execute tests for the CK25 dataset you will need a valid API according to the challenge specifications, [see](https://text2sparql.aksw.org/latest/participation/challenge/#process) and the directory `examples`. It contains the questions file `questions_db25.yml` the True Result-set `db25_true_result.json` (which can be generated with the questions file running the command `text2sparql query -o "db25_true_result.json" questions.yml`), and the example shell file. To ask, query and evaluate according to your API open the `examples` directory and run `run_db25.sh` passing `<API_IP> <API_NAME>`, for example:
39+
Usage example for the DB25 dataset from the [First International TEXT2SPARQL challenge](https://text2sparql.aksw.org/2025/). To execute tests for the CK25 dataset you will need a valid API according to the challenge specifications, [see](https://text2sparql.aksw.org/latest/participation/challenge/#process) and the directory `examples`. It contains the questions file `questions_db25.yml` the True Result-set `db25_true_result.json` (which can be generated with the questions file running the command `text2sparql query -o "db25_true_result.json" questions.yml`), and the example shell file. To ask, query and evaluate according to your API open the `examples` directory and run `run_db25.sh` passing `<API_URL> <API_NAME>`, for example:
4040

4141
```bash
4242
cd examples
43-
bash run_db25.sh "http://127.0.0.1:8000" "text2sparql_qwen3"
43+
bash run_db25.sh "http://localhost:8000" "text2sparql"
4444
```
4545

4646
## Commands Reference
@@ -86,6 +86,9 @@ Query a TEXT2SPARQL endpoint using a questions YAML file and send each question
8686
|--------|------|---------|-------------|
8787
| `--answers-db` | Path | `responses.db` | Where to save the endpoint responses (SQLite database) |
8888
| `--timeout` | Integer | `600` | Timeout in seconds for each request |
89+
| `--retries` / `-r` | Integer | `5` | Number of retries for disconnected, http error and timed out requests |
90+
| `--retry-sleep` | Integer | `15` | Seconds to sleep between retries |
91+
| `--retries-log` | Path | `retries.log` | File to log retries to |
8992
| `--output` / `-o` | Path | `-` (stdout) | Save JSON output to this file |
9093
| `--cache` / `--no-cache` | Boolean | `True` | If possible, return a cached response from the answers database |
9194

‎examples_ck25/run_ck25.sh‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
set -euo pipefail; export FS=$'\n\t'
55

66
API_NAME=${2:-}
7-
API_IP=${1:-}
7+
API_URL=${1:-}
88

9-
echo "Running ask, query and evaluate for all questions and responses for $API_NAME at $API_IP"
10-
text2sparql ask -o "${API_NAME}_ck25_answers.json" --answers-db "${API_NAME}_ck25_answers.db" questions_ck25.yml "${API_IP}"
9+
echo "Running ask, query and evaluate for all questions and responses for $API_NAME at $API_URL"
10+
text2sparql ask -o "${API_NAME}_ck25_answers.json" --answers-db "${API_NAME}_ck25_answers.db" --retries-log "${API_NAME}_ck25_retries.log" questions_ck25.yml "${API_URL}"
1111
text2sparql query -o "${API_NAME}_ck25_pred_result_set.json" -a "${API_NAME}_ck25_answers.json" questions_ck25.yml
1212
text2sparql evaluate -o "${API_NAME}_ck25_results.json" "${API_NAME}" ck25_true_result_set.json "${API_NAME}_ck25_pred_result_set.json"

‎examples_db25/run_db25.sh‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
set -euo pipefail; export FS=$'\n\t'
55

66
API_NAME=${2:-}
7-
API_IP=${1:-}
7+
API_URL=${1:-}
88

9-
echo "Running ask, query and evaluate for all questions and responses for $API_NAME at $API_IP"
10-
text2sparql ask -o "${API_NAME}_db25_answers.json" --answers-db "${API_NAME}_db25_answers.db" questions_db25.yml "${API_IP}"
9+
echo "Running ask, query and evaluate for all questions and responses for $API_NAME at $API_URL"
10+
text2sparql ask -o "${API_NAME}_db25_answers.json" --answers-db "${API_NAME}_db25_answers.db" --retries-log "${API_NAME}_db25_retries.log" questions_db25.yml "${API_URL}"
1111
text2sparql query -o "${API_NAME}_db25_pred_result_set.json" -a "${API_NAME}_db25_answers.json" -l "['en', 'es']" -e "http://141.57.8.18:9081/sparql" questions_db25.yml
1212
text2sparql evaluate -o "${API_NAME}_db25_results.json" -l "['en', 'es']" "${API_NAME}" db25_true_result_set.json "${API_NAME}_db25_pred_result_set.json"

‎tests/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,5 @@ def run_asserting_error(command: tuple[str, ...] | list[str], match: str) -> Ann
7272
"""Wrap the CliRunner, asserting exit 1 or more"""
7373
result = _run(command=command)
7474
assert result.exit_code >= 1, f"exit code should be 1 or more (but was {result.exit_code})"
75-
assert match in result.stdout or match in str(result.result)
75+
assert match in result.stdout or match in str(result.result) or match in str(result.output)
7676
return result

‎tests/test_ask.py‎

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""Test queries"""
22

3-
import pytest
4-
5-
from tests import run, run_asserting_error
3+
from tests import run, run_asserting_error, run_without_assertion
64
from tests.conftest import QuestionsFiles, ServerFixture, is_json_file
75

86

@@ -30,7 +28,31 @@ def test_non_successful_runs(server: ServerFixture, questions_files: QuestionsFi
3028
)
3129

3230

33-
@pytest.mark.skip(reason="tests that require output to be save are currently disabled")
31+
def test_timeout_handling_and_loggin(
32+
server: ServerFixture, questions_files: QuestionsFiles
33+
) -> None:
34+
"""Test timeout handling and logging requests."""
35+
command = (
36+
"ask",
37+
"--timeout",
38+
"1",
39+
"--retries",
40+
"2",
41+
"--retry-sleep",
42+
"0",
43+
"--retries-log",
44+
"-",
45+
"-o",
46+
"output.json",
47+
str(questions_files.with_ids),
48+
server.get_url(),
49+
)
50+
result = run_without_assertion(command=command)
51+
assert "Read timed out" in result.output
52+
assert "Retrying" in result.output
53+
assert "Maximum number of retries reached" in result.output
54+
55+
3456
def test_output(server: ServerFixture, questions_files: QuestionsFiles) -> None:
3557
"""Test different output files."""
3658
output = "output.json"
@@ -42,10 +64,9 @@ def test_output(server: ServerFixture, questions_files: QuestionsFiles) -> None:
4264
assert is_json_file(output), "Output file should be JSON."
4365

4466

45-
@pytest.mark.skip(reason="tests that require output to be save are currently disabled")
4667
def test_cached_response(server: ServerFixture, questions_files: QuestionsFiles) -> None:
4768
"""Test cached response."""
4869
command = ("ask", str(questions_files.with_ids), server.get_url())
49-
assert "Cached response found." not in run(command=command).stdout
50-
assert "Cached response found." in run(command=command).stdout
51-
assert "Cached response found." not in run(command=(*command, "--no-cache")).stdout
70+
assert "Cached response found." not in run(command=command).output
71+
assert "Cached response found." in run(command=command).output
72+
assert "Cached response found." not in run(command=(*command, "--no-cache")).output

‎tests/test_evaluate.py‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""Test evaluate"""
22

3-
import pytest
4-
5-
from tests import run, run_asserting_error, run_without_assertion
3+
from tests import run, run_asserting_error
64
from tests.conftest import ResultSetsFiles, is_json_file
75

86

@@ -20,17 +18,17 @@ def test_successful_evaluation(result_sets_files: ResultSetsFiles) -> None:
2018

2119
def test_language_evaluation_error(result_sets_files: ResultSetsFiles) -> None:
2220
"""Test language option error in evaluation."""
23-
result = run_without_assertion(
21+
run_asserting_error(
2422
command=(
2523
"evaluate",
2624
"-l",
2725
"en, de",
2826
"api_name",
2927
str(result_sets_files.result_set),
3028
str(result_sets_files.result_set),
31-
)
29+
),
30+
match="not a valid language list",
3231
)
33-
assert result.exit_code >= 1, f"exit code should be 1 or more (but was {result.exit_code})"
3432

3533

3634
def test_missing_question_true_result_set(result_sets_files: ResultSetsFiles) -> None:
@@ -46,7 +44,6 @@ def test_missing_question_true_result_set(result_sets_files: ResultSetsFiles) ->
4644
)
4745

4846

49-
@pytest.mark.skip(reason="tests that require output to be save are currently disabled")
5047
def test_output_evaluation(result_sets_files: ResultSetsFiles) -> None:
5148
"""Test evaluation with output file."""
5249
output = "output.json"

‎tests/test_query.py‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""Test query"""
22

3-
import pytest
4-
5-
from tests import run, run_asserting_error, run_without_assertion
3+
from tests import run, run_asserting_error
64
from tests.conftest import QuestionsFiles, ResponsesFiles, is_json_file
75

86

@@ -48,20 +46,19 @@ def test_language_query_error(
4846
questions_files: QuestionsFiles, responses_files: ResponsesFiles
4947
) -> None:
5048
"""Test query with language option error."""
51-
result = run_without_assertion(
49+
run_asserting_error(
5250
command=(
5351
"query",
5452
"-a",
5553
str(responses_files.responses),
5654
"-l",
5755
"en, de",
5856
str(questions_files.with_ids),
59-
)
57+
),
58+
match="not a valid language list",
6059
)
61-
assert result.exit_code >= 1, f"exit code should be 1 or more (but was {result.exit_code})"
6260

6361

64-
@pytest.mark.skip(reason="tests that require output to be save are currently disabled")
6562
def test_output_query(questions_files: QuestionsFiles, responses_files: ResponsesFiles) -> None:
6663
"""Test query with output file."""
6764
output = "output.json"

‎text2sparql_client/commands/ask.py‎

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
from io import TextIOWrapper
66
from pathlib import Path
7+
from time import sleep
78

89
import click
910
import requests
@@ -12,7 +13,7 @@
1213
from pydantic import ValidationError
1314

1415
from text2sparql_client.database import Database
15-
from text2sparql_client.models.questions_file import QuestionsFile
16+
from text2sparql_client.models.questions_file import Question, QuestionsFile
1617
from text2sparql_client.request import text2sparql
1718

1819

@@ -23,6 +24,67 @@ def check_output_file(file: str) -> None:
2324
sys.exit(1)
2425

2526

27+
def _retry_response( # noqa: PLR0913
28+
counter: int,
29+
retries: int,
30+
retry_sleep: int,
31+
responses: list,
32+
url: str,
33+
file_model: QuestionsFile,
34+
question_section: Question,
35+
language: str,
36+
question: str,
37+
database: Database,
38+
timeout: int,
39+
cache: bool,
40+
) -> None:
41+
qname = f"{file_model.dataset.prefix}:{question_section.id}-{language}"
42+
43+
if counter > retries:
44+
logger.bind(retry=True).error(
45+
f"{qname} | Maximum number of retries reached. Skipping question."
46+
)
47+
return
48+
49+
logger.bind(retry=True).info(
50+
f"{qname} | Retrying ({counter}/{retries}) after {retry_sleep} seconds..."
51+
)
52+
sleep(retry_sleep)
53+
try:
54+
response = text2sparql(
55+
endpoint=url,
56+
dataset=file_model.dataset.id,
57+
question=question,
58+
database=database,
59+
timeout=timeout,
60+
cache=cache,
61+
)
62+
answer: dict[str, str] = response.model_dump()
63+
if question_section.id and file_model.dataset.prefix:
64+
answer["qname"] = f"{file_model.dataset.prefix}:{question_section.id}-{language}"
65+
answer["uri"] = f"{file_model.dataset.id}{question_section.id}-{language}"
66+
responses.append(answer)
67+
except (requests.ConnectionError, requests.HTTPError, requests.ReadTimeout) as error:
68+
logger.bind(retry=True).warning(f"{qname} | {error}")
69+
_retry_response(
70+
counter=counter + 1,
71+
retries=retries,
72+
retry_sleep=retry_sleep,
73+
responses=responses,
74+
url=url,
75+
file_model=file_model,
76+
question_section=question_section,
77+
language=language,
78+
question=question,
79+
database=database,
80+
timeout=timeout,
81+
cache=cache,
82+
)
83+
except ValidationError as error:
84+
logger.debug(str(error))
85+
logger.error("validation error")
86+
87+
2688
@click.command(name="ask")
2789
@click.argument("QUESTIONS_FILE", type=click.File())
2890
@click.argument("URL", type=click.STRING)
@@ -40,6 +102,28 @@ def check_output_file(file: str) -> None:
40102
show_default=True,
41103
help="Timeout in seconds.",
42104
)
105+
@click.option(
106+
"--retries",
107+
"-r",
108+
type=int,
109+
default=5,
110+
show_default=True,
111+
help="Number of retries for disconnected, http error and timed out requests.",
112+
)
113+
@click.option(
114+
"--retry-sleep",
115+
type=int,
116+
default=15,
117+
show_default=True,
118+
help="Amount of seconds to sleep before retrying a request.",
119+
)
120+
@click.option(
121+
"--retries-log",
122+
type=click.Path(dir_okay=False, writable=True, file_okay=True, allow_dash=True),
123+
default="retries.log",
124+
show_default=True,
125+
help="File to log retries errors to.",
126+
)
43127
@click.option(
44128
"--output",
45129
"-o",
@@ -59,6 +143,9 @@ def ask_command( # noqa: PLR0913
59143
url: str,
60144
answers_db: str,
61145
timeout: int,
146+
retries: int,
147+
retry_sleep: int,
148+
retries_log: str,
62149
output: str,
63150
cache: bool,
64151
) -> None:
@@ -71,10 +158,12 @@ def ask_command( # noqa: PLR0913
71158
file_model = QuestionsFile.model_validate(yaml.safe_load(questions_file))
72159
logger.info(f"Asking questions about dataset {file_model.dataset.id} on endpoint {url}.")
73160
check_output_file(file=output)
161+
logger.add(retries_log, filter=lambda record: "retry" in record["extra"])
74162
responses = []
75163
for question_section in file_model.questions:
76164
for language, question in question_section.question.items():
77165
logger.info(f"{question} ({language}) ... ")
166+
qname = f"{file_model.dataset.prefix}:{question_section.id}-{language}"
78167
try:
79168
response = text2sparql(
80169
endpoint=url,
@@ -86,13 +175,25 @@ def ask_command( # noqa: PLR0913
86175
)
87176
answer: dict[str, str] = response.model_dump()
88177
if question_section.id and file_model.dataset.prefix:
89-
answer["qname"] = (
90-
f"{file_model.dataset.prefix}:{question_section.id}-{language}"
91-
)
178+
answer["qname"] = qname
92179
answer["uri"] = f"{file_model.dataset.id}{question_section.id}-{language}"
93180
responses.append(answer)
94181
except (requests.ConnectionError, requests.HTTPError, requests.ReadTimeout) as error:
95-
logger.error(str(error))
182+
logger.bind(retry=True).warning(f"{qname} | {error}")
183+
_retry_response(
184+
counter=1,
185+
retries=retries,
186+
retry_sleep=retry_sleep,
187+
responses=responses,
188+
url=url,
189+
file_model=file_model,
190+
question_section=question_section,
191+
language=language,
192+
question=question,
193+
database=database,
194+
timeout=timeout,
195+
cache=cache,
196+
)
96197
except ValidationError as error:
97198
logger.debug(str(error))
98199
logger.error("validation error")

0 commit comments

Comments
 (0)