Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
index.py
.vscode
__pycache__
__pycache__
.idea
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ All available API modules are supported - City feed, Geolocalized feed, Search,

### Installation

Coming soon...
The project is managed using Hatch ([documentation](https://hatch.pypa.io/latest/install/)). After Hatch is installed, proceed with commands

```bash

hatch env create # Creates virtual environment
hatch shell # Enable virtual environment
hatch build . # Builds the package wheel and sdist artifacts
```

It will provide the sdist and wheel artifacts that later can be installed via `pip`.

### Get API key

Expand All @@ -15,8 +24,11 @@ Sign up for an API key [here](https://aqicn.org/data-platform/token/)

The primary `WaqiAPI` class in the `waqi_api` module is a factory class that creates objects for each of the API modules, allowing you to make requests to any of them with your desired request parameters. You have to first create an object for it, then access your desired API module via the object. See the code snippets below:

```py
from waqi_api import WaqiAPI
```python

WAQI_TOKEN = 'Obtained API key'

from waqi_python_client.waqi_api import WaqiAPI

api = WaqiAPI(WAQI_TOKEN)
```
Expand Down Expand Up @@ -50,3 +62,10 @@ response = api.ip_feed().set_ip("MY_IP").fetch()
```py
response = api.map_station().set_map_bounds(40.712, -74.006, 34.052, -118.243).fetch()
```

## Tests

```bash
hatch shell
hatch test
```
27 changes: 24 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ build-backend = "hatchling.build"
name = "waqi-python-client"
version = "0.0.1"
authors = [
{ name="Nonso Mgbechi", email="[email protected]" },
{ name="Niyi Omotoso", email="[email protected]" },
{ name="Nonso Mgbechi", email="[email protected]" },
{ name="Niyi Omotoso", email="[email protected]" },
{ name="Andrey Sinitsyn", email="[email protected]" },
]
description = "Python client library for the World Air Quality Index (WAQI) APIs"
readme = "README.md"
Expand All @@ -17,7 +18,27 @@ classifiers = [
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"requests",
]

[project.urls]
"Homepage" = "https://github.com/waqi-dev-community/waqi-python-client"
"Bug Tracker" = "https://github.com/waqi-dev-community/waqi-python-client/issues"
"Bug Tracker" = "https://github.com/waqi-dev-community/waqi-python-client/issues"

[tool.hatch.build]
packages = ["src/waqi_python_client"]

[tool.hatch.build.targets.wheel]
packages = ["src/waqi_python_client"]

[tool.hatch.build.targets.sdist]
include = [
"/src",
"/tests",
]

[tool.pytest.ini_options]
pythonpath = [
"."
]
78 changes: 0 additions & 78 deletions src/waqi-python-client/entities.py

This file was deleted.

File renamed without changes.
82 changes: 82 additions & 0 deletions src/waqi_python_client/entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import requests
from typing import AnyStr, Any, Dict, Self, Union

BASE_URL = "https://api.waqi.info/"


class WaqiAPIEntity:
def __init__(self, api_key: AnyStr) -> None:
self.api_key = api_key
self.base_url = BASE_URL
self.query_params = {}

def set_query_param(self, param: AnyStr, value: Any) -> Self:
self.query_params[param] = value
return self # Return the class object to enable method chaining

def set_multiple_query_params(self, **kwargs) -> Self:
self.query_params.update(kwargs)
return self

def build_query_params(self) -> AnyStr:
if not self.query_params:
return ''
return '&' + '&'.join([f'{param}={value}' for param, value in self.query_params.items()])

def fetch(self, as_json=False) -> Union[AnyStr, Dict[AnyStr, Any]]:
url = self.url() + '?token=' + self.api_key + self.build_query_params()
response = requests.get(url)
return response.json() if as_json else response.text

def url(self) -> AnyStr:
raise NotImplementedError("Subclasses must implement url()")


class CityFeed(WaqiAPIEntity):
def set_city(self, city: AnyStr) -> Self:
return self.set_query_param('city', city)

def url(self) -> AnyStr:
# /feed/:city/?token=:token
url = f'{self.base_url}feed/{self.query_params["city"]}/'
self.query_params = {}
return url


class Search(WaqiAPIEntity):
def set_keyword(self, keyword: AnyStr) -> Self:
return self.set_query_param('keyword', keyword)

def url(self) -> AnyStr:
return f'{self.base_url}search/'


class GeoFeed(WaqiAPIEntity):
def set_coordinates(self, latitude: Union[int, float], longitude: Union[int, float]) -> Self:
return self.set_multiple_query_params(lat=latitude, lon=longitude)

def url(self) -> AnyStr:
# /feed/geo::lat;:lng/?token=:token
url = f'{self.base_url}feed/geo:{self.query_params["lat"]};{self.query_params["lon"]}/'
self.query_params = {}
return url


class MapStation(WaqiAPIEntity):
def set_map_bounds(
self, latitude_north: Union[int, float], longitude_west: Union[int, float],
latitude_south: Union[int, float], longitude_east: Union[int, float]):
return self.set_query_param(
'latlng', f"{latitude_north},{longitude_west},{latitude_south},{longitude_east}")


def url(self) -> AnyStr:
return f'{self.base_url}map/bounds/'


class IPFeed(WaqiAPIEntity):
def set_ip(self, ip_address: AnyStr):
return self.set_query_param('ip', ip_address)

def url(self) -> AnyStr:
return f"{self.base_url}feed/here/"
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from entities import CityFeed, IPFeed, Search, GeoFeed, MapStation
from .entities import CityFeed, IPFeed, Search, GeoFeed, MapStation


# Factory class for creating API objects
class WaqiAPI:
def __init__(self, api_key):
Expand All @@ -17,4 +19,4 @@ def ip_feed(self):
return IPFeed(self.api_key)

def map_station(self):
return MapStation(self.api_key)
return MapStation(self.api_key)
59 changes: 52 additions & 7 deletions tests/waqi_api_test.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import unittest
import requests
from unittest.mock import patch, Mock
import sys

# replace the path below with your own path to run this
sys.path.append('/home/nonso/Documents/Projects/AQI_SDKs/waqi-python-client/src/waqi-python-client')

# Import the classes from your Python code
from waqi_api import WaqiAPI
from src.waqi_python_client.waqi_api import WaqiAPI

class WaqiAPITest(unittest.TestCase):

Expand All @@ -31,6 +27,19 @@ def test_city_feed_api(self, mock_get):
self.assertEqual(response_city, '{"data": "city_feed_data"}')
mock_get.assert_called_once_with(f'https://api.waqi.info/feed/{city}/?token={self.api_key}')

@patch.object(requests, 'get')
def test_city_feed_api_json(self, mock_get):
mock_response = Mock()
mock_response.json.return_value = {"data": "city_feed_data"}
mock_get.return_value = mock_response

city_feed_api = self.factory.city_feed()
city = 'example_city'
response_city = city_feed_api.set_city(city).fetch(as_json=True)

self.assertEqual(response_city, {"data": "city_feed_data"})
mock_get.assert_called_once_with(f'https://api.waqi.info/feed/{city}/?token={self.api_key}')

@patch.object(requests, 'get')
def test_search_api(self, mock_get):
mock_response = Mock()
Expand All @@ -44,6 +53,19 @@ def test_search_api(self, mock_get):
self.assertEqual(response_search, '{"data": "search_data"}')
mock_get.assert_called_once_with(f'https://api.waqi.info/search/?token={self.api_key}&keyword={keyword}')

@patch.object(requests, 'get')
def test_search_api_json(self, mock_get):
mock_response = Mock()
mock_response.json.return_value = {"data": "search_data"}
mock_get.return_value = mock_response

search_api = self.factory.search()
keyword = "Johannesburg"
response_search = search_api.set_keyword(keyword).fetch(as_json=True)

self.assertEqual(response_search, {"data": "search_data"})
mock_get.assert_called_once_with(f'https://api.waqi.info/search/?token={self.api_key}&keyword={keyword}')

@patch.object(requests, 'get')
def test_geo_feed_api(self, mock_get):
mock_response = Mock()
Expand All @@ -59,6 +81,21 @@ def test_geo_feed_api(self, mock_get):
self.assertEqual(response_geo, '{"data": "geo_feed_data"}')
mock_get.assert_called_once_with(f'https://api.waqi.info/feed/geo:{lat};{lon}/?token={self.api_key}')

@patch.object(requests, 'get')
def test_geo_feed_api_json(self, mock_get):
mock_response = Mock()
mock_response.json.return_value = {"data": "geo_feed_data"}
mock_get.return_value = mock_response

geo_feed_api = self.factory.geo_feed()

lat = 37.7749
lon = -122.4194
response_geo = geo_feed_api.set_coordinates(lat, lon).fetch(as_json=True)

self.assertEqual(response_geo, {"data": "geo_feed_data"})
mock_get.assert_called_once_with(f'https://api.waqi.info/feed/geo:{lat};{lon}/?token={self.api_key}')

@patch.object(requests, 'get')
def test_map_stations_api(self, mock_get):
mock_response = Mock()
Expand All @@ -71,6 +108,14 @@ def test_map_stations_api(self, mock_get):
self.assertEqual(response_map_stations, '{"data": "map_stations_data"}')
mock_get.assert_called_once_with(f'https://api.waqi.info/map/bounds/?token={self.api_key}&latlng=40.712,-74.006,34.052,-118.243')

@patch.object(requests, 'get')
def test_map_stations_api_json(self, mock_get):
mock_response = Mock()
mock_response.json.return_value = {"data": "map_stations_data"}
mock_get.return_value = mock_response
map_stations_api = self.factory.map_station()

if __name__ == '__main__':
unittest.main()
response_map_stations = map_stations_api.set_map_bounds(40.712, -74.006, 34.052, -118.243).fetch(as_json=True)

self.assertEqual(response_map_stations, {"data": "map_stations_data"})
mock_get.assert_called_once_with(f'https://api.waqi.info/map/bounds/?token={self.api_key}&latlng=40.712,-74.006,34.052,-118.243')