Skip to content
This repository has been archived by the owner on Jun 7, 2022. It is now read-only.

Commit

Permalink
Merge pull request #92 from ReagentX/develop
Browse files Browse the repository at this point in the history
Purple Air Client v1.2.6
  • Loading branch information
ReagentX authored Feb 28, 2022
2 parents 7b096b5 + 5a36ed5 commit af0dd37
Show file tree
Hide file tree
Showing 10 changed files with 31 additions and 34 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PurpleAir API

[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4540629.svg)](https://doi.org/10.5281/zenodo.4540629)
[![DOI](https://zenodo.org/badge/163108208.svg)](https://zenodo.org/badge/latestdoi/163108208)

A Python 3.x module to turn data from the PurpleAir/ThingSpeak API into a Pandas DataFrame safely, with many utility methods and clear errors.

Expand Down Expand Up @@ -37,8 +37,9 @@ For detailed documentation, see the [docs](docs/documentation.md) file.

```python
from purpleair.network import SensorList
p = SensorList() # Initialized 11,220 sensors!
print(len(p.useful_sensors)) # 10047, List of sensors with no defects
p = SensorList() # Initialized 23,145 sensors!
useful = [s for s in p.all_sensors if s.is_useful()] # List of sensors with no defects
print(len(useful)) # 17,426
```

### Get location for a single sensor
Expand All @@ -54,7 +55,7 @@ print(s) # Sensor 2891 at 10834, Canyon Road, Omaha, Douglas County, Nebraska,
```python
from purpleair.network import SensorList
p = SensorList() # Initialized 11,220 sensors!
# Other sensor filters include 'outside', 'useful', 'family', and 'no_child'
# Other sensor filters include 'outside', 'useful', and 'family'
df = p.to_dataframe(sensor_filter='all',
channel='parent')
```
Expand Down
2 changes: 0 additions & 2 deletions docs/api/sensorlist_methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ Converts dictionary representation of a list of sensors to a Pandas DataFrame wh
* Do not filter sensors
* `family`
* Sensor has both parent and child
* `no_child`
* Sensor is parent-only
* `column`
* Must be a value that exists on a [Channel](/docs/documentation.md#channel)
* If `value_filter` is not provided:
Expand Down
Binary file modified maps/sensor_map.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions purpleair/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ def as_flat_dict(self) -> dict:
"""
out_d = {}
nested = self.as_dict()
# pylint: disable=consider-using-dict-items
for category in nested:
for prop in nested[category]:
out_d[prop] = nested[category][prop]
Expand Down
5 changes: 2 additions & 3 deletions purpleair/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def parse_raw_result(self, flat_sensor_data: dict) -> None:
parent_map[sensor['ID']] = sensor

# Second pass: build list of complete sensors
# pylint: disable=consider-using-dict-items
for child_sensor_id in child_map:
parent_sensor_id = child_map[child_sensor_id]['ParentID']
if parent_sensor_id not in parent_map:
Expand All @@ -84,6 +85,7 @@ def parse_raw_result(self, flat_sensor_data: dict) -> None:
out_l.append(channels)

# Handle remaining parent sensors
# pylint: disable=consider-using-dict-items
for remaining_parent in parent_map:
channels = [
parent_map[remaining_parent],
Expand Down Expand Up @@ -173,9 +175,6 @@ def to_dataframe(self,
'family': lambda: pd.DataFrame([s.as_flat_dict(channel)
for s in [s for s in self.all_sensors
if s.parent and s.child]]),
'no_child': lambda: pd.DataFrame([s.as_flat_dict(channel)
for s in [s for s in self.all_sensors
if not s.child]]),
'column': lambda: self.filter_column(channel, column, value_filter)
}[sensor_filter]()
except KeyError as err:
Expand Down
19 changes: 10 additions & 9 deletions purpleair/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ class Sensor():
Representation of a single PurpleAir sensor
"""

def __init__(self, identifier: int, json_data: list = None, parse_location=False):
self.identifier = identifier
def __init__(self, identifier: int, json_data: Optional[list] = None, parse_location=False):
self.data: Optional[list] = json_data \
if json_data is not None else self.get_data()
if json_data is not None else self.get_data(identifier)

# Validate the data we received
if not self.data:
Expand All @@ -35,6 +34,7 @@ def __init__(self, identifier: int, json_data: list = None, parse_location=False
f'Sensor {identifier} created without valid data')

self.parent_data: dict = self.data[0]
self.identifier = self.parent_data['ID']
self.child_data: Optional[dict] = self.data[1] if len(
self.data) > 1 else None
self.parse_location: bool = parse_location
Expand All @@ -48,17 +48,18 @@ def __init__(self, identifier: int, json_data: list = None, parse_location=False
if self.parse_location:
self.get_location()

def get_data(self) -> Optional[list]:
# pylint: disable=no-self-use
def get_data(self, identifier: int) -> Optional[list]:
"""
Get new data if no data is provided
"""
# Sanitize ID
if not isinstance(self.identifier, int):
raise ValueError(f'Invalid sensor ID: {self.identifier}')
if not isinstance(identifier, int):
raise ValueError(f'Invalid sensor ID: {identifier}')

# Fetch the JSON for parent and child sensors
session = CachedSession(expire_after=timedelta(hours=1))
response = session.get(f'{API_ROOT}?show={self.identifier}')
response = session.get(f'{API_ROOT}?show={identifier}')
data = json.loads(response.content)
channel_data: Optional[list] = data.get('results')

Expand All @@ -69,14 +70,14 @@ def get_data(self) -> Optional[list]:
parent_id = channel_data[0]["ParentID"]
except IndexError:
raise IndexError from IndexError(
f'Parent sensor for {self.identifier} does not exist!')
f'Parent sensor for {identifier} does not exist!')
response = session.get(f'{API_ROOT}?show={parent_id}')
data = json.loads(response.content)
channel_data = data.get('results')
elif channel_data and len(channel_data) > 2:
print(json.dumps(data, indent=4))
raise ValueError(
f'More than 2 channels found for {self.identifier}')
f'More than 2 channels found for {identifier}')
return channel_data

def get_field(self, field: int) -> None:
Expand Down
8 changes: 4 additions & 4 deletions requirements/common.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Usage requirements
requests_cache==0.6.0
requests_cache==0.9.3
thingspeak==1.0.0
requests==2.25.1
pandas==1.2.4
geopy==2.1.0
requests==2.27.1
pandas==1.4.1
geopy==2.2.0
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

setup(
name='purpleair',
version='1.2.5',
version='1.2.6',
description='Python API Client to get and transform PurpleAir data.',
long_description=LONG_DESCRIPTION,
long_description_content_type="text/markdown",
Expand Down
11 changes: 3 additions & 8 deletions tests/test_purpleair.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,6 @@ def test_to_dataframe_filtering_useful(self):
p.to_dataframe('useful', 'parent')
p.to_dataframe('useful', 'child')

def test_to_dataframe_filtering_no_child(self):
"""
Test that no_child sensor filter works
"""
p = network.SensorList()
p.to_dataframe('no_child', 'parent')
p.to_dataframe('no_child', 'child')

def test_to_dataframe_filtering_family(self):
"""
Test that family sensor filter works
Expand All @@ -56,6 +48,9 @@ def test_to_dataframe_filtering_family(self):
p.to_dataframe('family', 'child')

def test_to_dataframe_cols(self):
"""
Test that child and parent sensor dataframes contain the same data
"""
p = network.SensorList()
df_a = p.to_dataframe(sensor_filter='all', channel='parent')
df_b = p.to_dataframe(sensor_filter='all', channel='child')
Expand Down
8 changes: 5 additions & 3 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_create_sensor_location(self):
se = sensor.Sensor(2891, parse_location=True)
self.assertEqual(
se.__repr__(),
'Sensor 2891 at 10834, Canyon Road, Omaha, Douglas County, Nebraska, 68112, United States'
'Sensor 2890 at 10834, Canyon Road, Omaha, Douglas County, Nebraska, 68112, United States'
)

def test_can_get_field(self):
Expand Down Expand Up @@ -52,15 +52,17 @@ def test_cannot_create_sensor_bad_json(self):
def test_create_sensor_no_location(self):
"""
Test that we can initialize a sensor without location enabled
This test gets a child sensor, so we end up with the parent's ID
"""
se = sensor.Sensor(2891)
self.assertEqual(se.__repr__(), 'Sensor 2891')
self.assertEqual(se.__repr__(), 'Sensor 2890')

def test_is_useful(self):
"""
Test that we ensure a useful sensor is useful
"""
se = sensor.Sensor(14633)
se = sensor.Sensor(81879)
self.assertEqual(se.is_useful(), True)

def test_is_not_useful_flagged(self):
Expand Down

0 comments on commit af0dd37

Please sign in to comment.