Skip to content

Commit 2bcac87

Browse files
authored
v0.6.0 (#54)
* Query parameter validation (#53) * add validator functions * fix datetime validator to support python 3.10 * Type checking coverage (#55) * type fixes * restructure and add typing for mypy reporting * Mypy action (#56) * add types action, py.typed file and updates to pyproject * type fixes * mypy ignores * fix orjson import for mypy * property tests initial (#57) * Validation test (#58) * add tests * basic sync resource validation check test * addtional locations tests * tests * locations parameters tests * parameters tests * countries and licenses tests * instruments tests * manufacturers test * remove unecessary mocks and remove hypothesis test * remove unused function paraeters in tests * add owners tests * providers test * fix licenses mock * fix license * refactor resource tests initial * refactor sync resource tests * add measurements and locations latest tests * add async resource tests * bump version and changelog
1 parent ba1106e commit 2bcac87

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+7991
-814
lines changed

.github/workflows/types.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: MyPy Type Check
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
mypy:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: '3.10'
21+
22+
- name: Install Hatch
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install hatch
26+
27+
- name: Run mypy
28+
run: |
29+
hatch run types:ci

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ __pycache__
66
.ruff_cache
77
site
88
.vscode
9-
coverage.xml
9+
coverage.xml
10+
.hypothesis

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

7-
## [0.5.0] - Unreleased
7+
## [0.6.0] - 2025-11-26
8+
9+
### Added
10+
11+
- Query parameter validation
12+
- Additional test coverage
13+
- mypy typing coverage
14+
- `py.typed` file
15+
16+
## [0.5.0] - 2025-10-31
817

918
### Updated
1019

@@ -13,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1322

1423
### Added
1524

25+
- Additional checks to validate query parameters.
1626
- Additional checks to prevent out of range identifiers.
1727
- `TimeoutError` HTTP error exception.
1828

openaq/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44

5-
__version__ = "0.5.0"
5+
__version__ = "0.6.0"
66

77

88
logger = logging.getLogger("openaq")

openaq/_async/client.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from __future__ import annotations
22

3+
import httpx
4+
5+
from types import TracebackType
36
from typing import Any, Mapping
47

58
from openaq._async.models.countries import Countries
@@ -85,9 +88,11 @@ async def _do(
8588
method: str,
8689
path: str,
8790
*,
88-
params: Mapping[str, Any] | None = None,
89-
headers: Mapping[str, str] | None = None,
90-
):
91+
params: (
92+
httpx.QueryParams | Mapping[str, str | int | float | bool] | None
93+
) = None,
94+
headers: httpx.Headers | Mapping[str, str] | None = None,
95+
) -> httpx.Response:
9196
self._check_rate_limit()
9297
request_headers = self.build_request_headers(headers)
9398
url = self._base_url + path
@@ -100,16 +105,24 @@ async def _get(
100105
self,
101106
path: str,
102107
*,
103-
params: Mapping[str, str] | None = None,
104-
headers: Mapping[str, Any] | None = None,
105-
):
108+
params: (
109+
httpx.QueryParams | Mapping[str, str | int | float | bool] | None
110+
) = None,
111+
headers: httpx.Headers | Mapping[str, str] | None = None,
112+
) -> httpx.Response:
106113
return await self._do("get", path, params=params, headers=headers)
107114

108-
async def close(self):
109-
await self._transport.close()
115+
async def close(self) -> None:
116+
"""Closes transport connection."""
117+
return await self.transport.close()
110118

111119
async def __aenter__(self) -> AsyncOpenAQ:
112120
return self
113121

114-
async def __aexit__(self, *_: Any):
122+
async def __aexit__(
123+
self,
124+
exc_type: type[BaseException] | None,
125+
exc_val: BaseException | None,
126+
exc_tb: TracebackType | None,
127+
) -> None:
115128
await self.close()

openaq/_async/models/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from typing import TYPE_CHECKING
24

35
if TYPE_CHECKING:
@@ -17,7 +19,7 @@ class AsyncResourceBase:
1719

1820
def __init__(
1921
self,
20-
client: "AsyncOpenAQ",
22+
client: AsyncOpenAQ,
2123
):
2224
"""Initialize the SyncResourceBase.
2325

openaq/_async/models/countries.py

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
from openaq.shared.models import build_query_params
22
from openaq.shared.responses import CountriesResponse
3-
from openaq.shared.utils import validate_integer_id
4-
3+
from openaq.shared.types import SortOrder
4+
from openaq.shared.validators import (
5+
validate_integer_id,
6+
validate_integer_or_list_integer_params,
7+
validate_limit_param,
8+
validate_order_by,
9+
validate_page_param,
10+
validate_sort_order,
11+
)
512
from .base import AsyncResourceBase
613

714

@@ -42,33 +49,25 @@ async def list(
4249
page: int = 1,
4350
limit: int = 1000,
4451
order_by: str | None = None,
45-
sort_order: str | None = None,
46-
parameters_id: int | None = None,
47-
providers_id: int | None = None,
52+
sort_order: SortOrder | None = None,
53+
parameters_id: int | list[int] | None = None,
54+
providers_id: int | list[int] | None = None,
4855
) -> CountriesResponse:
4956
"""List countries based on provided filters.
5057
51-
Provides the ability to filter the countries resource by the given arguments.
52-
53-
* `page` - Specifies the page number of results to retrieve
54-
* `limit` - Sets the number of results generated per page
55-
* `providers_id` - Filter results by selected providers ID(s)
56-
* `parameters_id` - Filters results by selected parameters ID(s)
57-
* `order_by` - Determines the fields by which results are sorted; available values are `id`
58-
* `sort_order` - Works in tandem with `order_by` to specify the direction: either `asc` (ascending) or `desc` (descending)
59-
6058
Args:
61-
page: The page number. Page count is countries found / limit.
62-
limit: The number of results returned per page.
59+
page: The page number, must be greater than zero. Page count is countries found / limit.
60+
limit: The number of results returned per page. Must be between 1 and 1,000.
6361
order_by: Order by operators for results.
64-
sort_order: Sort order (asc/desc).
62+
sort_order: Order for sorting results (asc/desc).
6563
parameters_id: Single parameters ID or an array of IDs.
6664
providers_id: Single providers ID or an array of IDs.
6765
6866
Returns:
6967
CountriesResponse: An instance representing the list of retrieved countries.
7068
7169
Raises:
70+
InvalidParameterError: Client validation error, query parameter is not correct type or value.
7271
IdentifierOutOfBoundsError: Client validation error, identifier outside support int32 range.
7372
ApiKeyMissingError: Authentication error, missing API Key credentials.
7473
BadRequestError: Raised for HTTP 400 error, indicating a client request error.
@@ -84,6 +83,28 @@ async def list(
8483
ServiceUnavailableError: Raised for HTTP 503, indicating that the server is not ready to handle the request.
8584
GatewayTimeoutError: Raised for HTTP 504 error, indicating a gateway timeout.
8685
"""
86+
page = validate_page_param(page)
87+
limit = validate_limit_param(limit)
88+
if sort_order is not None:
89+
sort_order = validate_sort_order(sort_order)
90+
if order_by is not None:
91+
order_by = validate_order_by(order_by)
92+
if parameters_id is not None:
93+
parameters_id = validate_integer_or_list_integer_params(
94+
'parameters_id', parameters_id
95+
)
96+
if providers_id is not None:
97+
providers_id = validate_integer_or_list_integer_params(
98+
'providers_id', providers_id
99+
)
100+
params = build_query_params(
101+
page=page,
102+
limit=limit,
103+
order_by=order_by,
104+
sort_order=sort_order,
105+
parameters_id=parameters_id,
106+
providers_id=providers_id,
107+
)
87108
params = build_query_params(
88109
page=page,
89110
limit=limit,

openaq/_async/models/instruments.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
from openaq.shared.models import build_query_params
22
from openaq.shared.responses import InstrumentsResponse
3-
from openaq.shared.utils import validate_integer_id
4-
3+
from openaq.shared.types import SortOrder
4+
from openaq.shared.validators import (
5+
validate_integer_id,
6+
validate_limit_param,
7+
validate_order_by,
8+
validate_page_param,
9+
validate_sort_order,
10+
)
511
from .base import AsyncResourceBase
612

713

814
class Instruments(AsyncResourceBase):
915
"""This provides methods to retrieve instrument data from the OpenAQ API."""
1016

1117
async def get(self, instruments_id: int) -> InstrumentsResponse:
12-
"""Retrieve specific instrument data by its providers ID.
18+
"""Retrieve specific instrument data by its instruments ID.
1319
1420
Args:
15-
instruments_id: The providers ID of the instrument to retrieve.
21+
instruments_id: The instruments ID of the instrument to retrieve.
1622
1723
Returns:
1824
InstrumentsResponse: An instance representing the retrieved instrument.
@@ -42,27 +48,21 @@ async def list(
4248
page: int = 1,
4349
limit: int = 1000,
4450
order_by: str | None = None,
45-
sort_order: str | None = None,
51+
sort_order: SortOrder | None = None,
4652
) -> InstrumentsResponse:
4753
"""List instruments based on provided filters.
4854
49-
Provides the ability to filter the instruments resource by the given arguments.
50-
51-
* `page` - Specifies the page number of results to retrieve.
52-
* `limit` - Sets the number of results generated per page
53-
* `order_by` - Determines the fields by which results are sorted; available values are `id`
54-
* `sort_order` - Works in tandem with `order_by` to specify the direction: either `asc` (ascending) or `desc` (descending)
55-
5655
Args:
57-
page: The page number. Page count is instruments found / limit.
58-
limit: The number of results returned per page.
56+
page: The page number, must be greater than zero. Page count is instruments found / limit.
57+
limit: The number of results returned per page. Must be between 1 and 1,000.
5958
order_by: Order by operators for results.
60-
sort_order: Sort order (asc/desc).
59+
sort_order: Order for sorting results (asc/desc).
6160
6261
Returns:
6362
InstrumentsResponse: An instance representing the list of retrieved instruments.
6463
6564
Raises:
65+
InvalidParameterError: Client validation error, query parameter is not correct type or value.
6666
IdentifierOutOfBoundsError: Client validation error, identifier outside support int32 range.
6767
ApiKeyMissingError: Authentication error, missing API Key credentials.
6868
BadRequestError: Raised for HTTP 400 error, indicating a client request error.
@@ -78,6 +78,12 @@ async def list(
7878
ServiceUnavailableError: Raised for HTTP 503, indicating that the server is not ready to handle the request.
7979
GatewayTimeoutError: Raised for HTTP 504 error, indicating a gateway timeout.
8080
"""
81+
page = validate_page_param(page)
82+
limit = validate_limit_param(limit)
83+
if sort_order is not None:
84+
sort_order = validate_sort_order(sort_order)
85+
if order_by is not None:
86+
order_by = validate_order_by(order_by)
8187
params = build_query_params(
8288
page=page, limit=limit, order_by=order_by, sort_order=sort_order
8389
)

openaq/_async/models/licenses.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from openaq.shared.models import build_query_params
22
from openaq.shared.responses import LicensesResponse
3-
from openaq.shared.utils import validate_integer_id
4-
3+
from openaq.shared.types import SortOrder
4+
from openaq.shared.validators import (
5+
validate_integer_id,
6+
validate_limit_param,
7+
validate_order_by,
8+
validate_page_param,
9+
validate_sort_order,
10+
)
511
from .base import AsyncResourceBase
612

713

@@ -42,27 +48,21 @@ async def list(
4248
page: int = 1,
4349
limit: int = 1000,
4450
order_by: str | None = None,
45-
sort_order: str | None = None,
51+
sort_order: SortOrder | None = None,
4652
) -> LicensesResponse:
4753
"""List licenses based on provided filters.
4854
49-
Provides the ability to filter the locations resource by the given arguments.
50-
51-
* `page` - Specifies the page number of results to retrieve
52-
* `limit` - Sets the number of results generated per page
53-
* `order_by` - Determines the fields by which results are sorted; available values are `id`
54-
* `sort_order` - Works in tandem with `order_by` to specify the direction: either `asc` (ascending) or `desc` (descending)
55-
5655
Args:
57-
page: The page number. Page count is locations found / limit.
58-
limit: The number of results returned per page.
56+
page: The page number, must be greater than zero. Page count is licenses found / limit.
57+
limit: The number of results returned per page. Must be between 1 and 1,000.
5958
order_by: Order by operators for results.
60-
sort_order: Sort order (asc/desc).
59+
sort_order: Order for sorting results (asc/desc).
6160
6261
Returns:
6362
LicensesReponse: An instance representing the list of retrieved licenses.
6463
6564
Raises:
65+
InvalidParameterError: Client validation error, query parameter is not correct type or value.
6666
IdentifierOutOfBoundsError: Client validation error, identifier outside support int32 range.
6767
ApiKeyMissingError: Authentication error, missing API Key credentials.
6868
BadRequestError: Raised for HTTP 400 error, indicating a client request error.
@@ -78,6 +78,12 @@ async def list(
7878
ServiceUnavailableError: Raised for HTTP 503, indicating that the server is not ready to handle the request.
7979
GatewayTimeoutError: Raised for HTTP 504 error, indicating a gateway timeout.
8080
"""
81+
page = validate_page_param(page)
82+
limit = validate_limit_param(limit)
83+
if sort_order is not None:
84+
sort_order = validate_sort_order(sort_order)
85+
if order_by is not None:
86+
order_by = validate_order_by(order_by)
8187
params = build_query_params(
8288
page=page,
8389
limit=limit,

0 commit comments

Comments
 (0)