Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for passing a list to filters in list_measurements #929

Merged
merged 3 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ class MeasurementBase(BaseModel):
probe_asn: Optional[str] = Field(default=None, title="ASN of the measurement probe")
probe_cc: Optional[str] = Field(default=None, title="country code of the probe ASN")
report_id: Optional[str] = Field(default=None, title="report id of the measurement")
scores: Optional[str] = Field(
scores: Optional[Dict[str, object]] = Field(
default=None, title="blocking scores of the measurement"
)
test_name: Optional[str] = Field(default=None, title="test name of the measurement")
Expand Down Expand Up @@ -531,46 +531,46 @@ def genurl(base_url: str, path: str, **kw) -> str:
async def list_measurements(
request: Request,
response: Response,
report_id: Annotated[
Optional[str],
Query(description="Report_id to search measurements for", min_length=3),
],
input: Annotated[
Optional[str],
Query(
description="Input (for example a URL or IP address) to search measurements for",
min_length=3,
),
],
domain: Annotated[
Optional[str],
Query(description="Domain to search measurements for", min_length=3),
],
probe_cc: Annotated[Optional[str], Query(description="Two letter country code")],
report_id: Optional[str] = Query(
None,
description="Report_id to search measurements for",
min_length=3,
),
input: Optional[str] = Query(
None,
description="Input (for example a URL or IP address) to search measurements for",
min_length=3,
),
domain: Optional[str] = Query(
None,
description="Domain to search measurements for",
min_length=3,
),
probe_cc: Annotated[Optional[str], Query(description="Two letter country code")] = None,
probe_asn: Annotated[
Union[str, int, None],
Query(description='Autonomous system number in the format "ASXXX"'),
],
] = None,
test_name: Annotated[
Optional[str],
Query(description="Name of the test"),
],
] = None,
category_code: Annotated[
Optional[str],
Query(description="Category code from the citizenlab list"),
],
] = None,
since: Annotated[
Optional[str],
Query(
description='Start date of when measurements were run (ex. "2016-10-20T10:30:00")'
),
],
] = None,
until: Annotated[
Optional[str],
Query(
description='End date of when measurement were run (ex. "2016-10-20T10:30:00")'
),
],
] = None,
confirmed: Annotated[
Optional[bool],
Query(
Expand All @@ -579,7 +579,7 @@ async def list_measurements(
"Default: no filtering (show both true and false)"
)
),
],
] = None,
anomaly: Annotated[
Optional[bool],
Query(
Expand All @@ -588,7 +588,7 @@ async def list_measurements(
"Default: no filtering (show both true and false)"
)
),
],
] = None,
failure: Annotated[
Optional[bool],
Query(
Expand All @@ -597,22 +597,22 @@ async def list_measurements(
"Default: no filtering (show both true and false)"
)
),
],
] = None,
software_version: Annotated[
Optional[str],
Query(description="Filter measurements by software version. Comma-separated."),
],
] = None,
test_version: Annotated[
Optional[str],
Query(description="Filter measurements by test version. Comma-separated."),
],
] = None,
engine_version: Annotated[
Optional[str],
Query(description="Filter measurements by engine version. Comma-separated."),
],
] = None,
ooni_run_link_id: Annotated[
Optional[str], Query(description="Filter measurements by OONIRun ID.")
],
] = None,
order_by: Annotated[
Optional[str],
Query(
Expand All @@ -626,7 +626,7 @@ async def list_measurements(
"test_name",
],
),
],
] = None,
order: Annotated[
str,
Query(
Expand All @@ -651,12 +651,6 @@ async def list_measurements(
# - lang: 'curl'
# source: |
# curl "https://api.ooni.io/api/v1/measurements?probe_cc=IT&confirmed=true&since=2017-09-01"
if (
probe_asn is not None
and isinstance(probe_asn, str)
and probe_asn.startswith("AS")
):
probe_asn = int(probe_asn[2:])
software_versions = None
if software_version:
software_versions = commasplit(software_version)
Expand Down Expand Up @@ -735,28 +729,31 @@ async def list_measurements(
fpwhere.append(sql.text("report_id = :report_id"))

if probe_cc:
if probe_cc == "ZZ":
log.info("Refusing list_measurements with probe_cc set to ZZ")
raise AbortMeasurementList
query_params["probe_cc"] = probe_cc
fpwhere.append(sql.text("probe_cc = :probe_cc"))
probe_cc_list = probe_cc.split(",")
query_params["probe_cc"] = probe_cc_list
fpwhere.append(sql.text("probe_cc IN :probe_cc"))
else:
fpwhere.append(sql.text("probe_cc != 'ZZ'"))

if probe_asn is not None:
if probe_asn == 0:
log.info("Refusing list_measurements with probe_asn set to 0")
raise AbortMeasurementList
query_params["probe_asn"] = probe_asn
fpwhere.append(sql.text("probe_asn = :probe_asn"))
if isinstance(probe_asn, str):
probe_asn_list = probe_asn.split(",")
probe_asn_integer_list = []
for probe_asn_value in probe_asn_list:
if probe_asn_value.startswith("AS"):
probe_asn_integer_list.append(int(probe_asn_value[2:]))
query_params["probe_asn"] = probe_asn_integer_list
fpwhere.append(sql.text("probe_asn IN :probe_asn"))

else:
# https://ooni.org/post/2020-ooni-probe-asn-incident-report/
# https://github.com/ooni/explorer/issues/495
fpwhere.append(sql.text("probe_asn != 0"))

if test_name is not None:
query_params["test_name"] = test_name
fpwhere.append(sql.text("test_name = :test_name"))
test_name_list = test_name.split(",")
query_params["test_name"] = test_name_list
fpwhere.append(sql.text("test_name IN :test_name"))

if software_versions is not None:
query_params["software_versions"] = software_versions
Expand Down Expand Up @@ -809,8 +806,9 @@ async def list_measurements(
elif domain or category_code:
# both domain and category_code can be set at the same time
if domain:
query_params["domain"] = domain
fpwhere.append(sql.text("domain = :domain"))
domain_list = domain.split(",")
query_params["domain"] = domain_list
fpwhere.append(sql.text("domain IN :domain"))

if category_code:
query_params["category_code"] = category_code
Expand Down Expand Up @@ -853,30 +851,14 @@ async def list_measurements(
probe_asn="AS{}".format(row["probe_asn"]),
test_name=row["test_name"],
measurement_start_time=row["measurement_start_time"],
input=row["input"],
input_=row["input"],
anomaly=row["anomaly"] == "t", # TODO: This is wrong
confirmed=row["confirmed"] == "t",
failure=row["msm_failure"] == "t",
scores=json.loads(row["scores"]),
)
)

results.append(
{
"measurement_uid": msmt_uid,
"measurement_url": url,
"report_id": row["report_id"],
"probe_cc": row["probe_cc"],
"probe_asn": "AS{}".format(row["probe_asn"]),
"test_name": row["test_name"],
"measurement_start_time": row["measurement_start_time"],
"input": row["input"],
"anomaly": row["anomaly"] == "t",
"confirmed": row["confirmed"] == "t",
"failure": row["msm_failure"] == "t",
"scores": json.loads(row["scores"]),
}
)
except OperationalError as exc:
log.error(exc)
if isinstance(exc.orig, QueryCanceledError):
Expand All @@ -889,8 +871,8 @@ async def list_measurements(

# Replace the special value INULL for "input" with None
for i, r in enumerate(results):
if r["input"] == INULL:
results[i]["input"] = None
if r.input_ == INULL:
results[i].input_ = None

pages = -1
count = -1
Expand Down
91 changes: 91 additions & 0 deletions ooniapi/services/oonimeasurements/tests/test_measurements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import pytest


route = "api/v1/measurements"


def test_list_measurements(client):
response = client.get(route)
json = response.json()

assert isinstance(json["results"], list), json
assert len(json["results"]) == 100


def test_list_measurements_with_since_and_until(client):
params = {
"since": "2024-01-01",
"until": "2024-01-02",
}

response = client.get(route, params=params)
json = response.json()

assert isinstance(json["results"], list), json
assert len(json["results"]) == 100


@pytest.mark.parametrize(
"filter_param, filter_value",
[
("test_name", "web_connectivity"),
("probe_cc", "IT"),
("probe_asn", "AS30722"),
]
)
def test_list_measurements_with_one_value_to_filters(client, filter_param, filter_value):
params = {}
params[filter_param] = filter_value

response = client.get(route, params=params)

json = response.json()
assert isinstance(json["results"], list), json
assert len(json["results"]) > 0
for result in json["results"]:
assert result[filter_param] == filter_value, result


def test_list_measurements_with_one_value_to_filters_not_in_the_result(client):
params = {
"domain": "cloudflare-dns.com",
}

response = client.get(route, params=params)

json = response.json()
assert isinstance(json["results"], list), json
assert len(json["results"]) > 0


@pytest.mark.parametrize(
"filter_param, filter_value",
[
("test_name", "web_connectivity,dnscheck,stunreachability,tor"),
("probe_cc", "IT,US,RU"),
("probe_asn", "AS30722,3269,7738,55430"),
]
)
def test_list_measurements_with_multiple_values_to_filters(client, filter_param, filter_value):
params = {}
params[filter_param] = filter_value

response = client.get(route, params=params)

json = response.json()
assert isinstance(json["results"], list), json
assert len(json["results"]) > 0
for result in json["results"]:
assert result[filter_param] in filter_value, result


def test_list_measurements_with_multiple_values_to_filters_not_in_the_result(client):
params = {
"domain": "cloudflare-dns.com, adblock.doh.mullvad.net, 1.1.1.1",
}

response = client.get(route, params=params)

json = response.json()
assert isinstance(json["results"], list), json
assert len(json["results"]) > 0
Loading